useState 与 useRef
探讨一下 useState 与 useRef,useMemo 和 useCallback
开门见山
现有如下 react native 代码,这是一个很常见的 todo list
import React, {useState, memo, useRef} from 'react';
import {
StyleSheet,
FlatList,
Button,
Text,
View,
Modal,
TextInput,
TouchableOpacity,
} from 'react-native';
function Home() {
const initData = [];
for (let i = 1; i <= 10; i++) {
initData.push({name: 'jack' + i, id: i.toString()});
}
const [data, setData] = useState(initData);
const [input, setInput] = useState('');
const renderItem = ({item}) => {
console.log('renderItem');
return (
<Text style={styles.text}>{item.name}</Text>
);
};
const addTodo = () => {
setInput('');
setData([{name: input, id: Math.random()}, ...data]);
};
const keyExtractor = (item, index) => {
return item.id;
};
return (
<View>
<TextInput
value={input}
onChangeText={setInput}
/>
<Button title={'新增'} onPress={addTodo}></Button>
<FlatList
showsVerticalScrollIndicator={false}
keyExtractor={keyExtractor}
data={data}
renderItem={renderItem}
/>
</View>
);
}
const styles = StyleSheet.create({
text: {
fontSize: 20,
padding: 10,
margin: 20,
backgroundColor: 'lavender',
textAlign: 'center',
},
});
export default Home;
可以发现,每次在 TextInput 输入一个字母,控制台都会打印10次 renderItem,意味着每次输入,都会触发整个 Flatlist 的重新渲染。
因为 TextInput 的值被保存在 Home 组件的状态中,每次输入框的值改变时,都会触发 setInput 函数,进而更新 input 的状态,导致 Home 组件重新渲染。
FlatList 作为 Home 的一个子组件,也会被重新渲染。
用 useRef 代替 useState
useState 是一个用于管理组件状态的 Hook。当使用 useState 更新状态时,React 会自动重新渲染组件以反映新的状态值。
useRef 则是一个用于存储可变值的 Hook,这些值在组件的重新渲染之间保持不变。
useRef 在其值发生变化时不会触发组件的重新渲染。useRef 返回一个可变的对象,其 .current 属性可以用于获取或设置值
在这个例子中,在 textInput 输入文字时,就非常适合用 useRef。这应该是最合理、最省力的方法。
代码更改如下:
// const [input, setInput] = useState('');
const inputRef = useRef(null);
addTodo 函数,获取输入值与清空输入框的方法发生变化
const addTodo = () => {
setData([{name: inputRef.current.value, id: Math.random()}, ...data]);
inputRef.current.clear();
};
视图绑定,使用 ref
<TextInput
ref={inputRef}
onChangeText={t => (inputRef.current.value = t)}
/>
抽取 textInput 与 Button
上边提到,Home 组件会重新渲染,是因为 TextInput 的值被保存在 Home 组件的状态中。那么我们把 TextInput 抽取出来,在它内部维护状态,就不会影响 Home。
这个方法的缺点是,需要把 addTodo 函数传递到 MyInput,在业务不复杂的情况,这样操作还是可以的。
代码如下
function MyInput({addTodo}) {
const [input, setInput] = useState('');
return (
<View>
<TextInput
value={input}
onChangeText={setInput}
/>
<Button title={'新增'} onPress={()=>{
addTodo(input);
setInput('');
}}></Button>
</View>
);
}
function Home() {
const initData = [];
for (let i = 1; i <= 10; i++) {
initData.push({name: 'jack' + i, id: i.toString()});
}
const [data, setData] = useState(initData);
const renderItem = ({item}) => {
console.log('renderItem');
return (
<Text style={styles.text}>{item.name}</Text>
);
};
const addTodo = (input) => {
setData([{name: input, id: Math.random()}, ...data]);
};
const keyExtractor = (item, index) => {
return item.id;
};
return (
<View>
<MyInput addTodo={addTodo}/>
<FlatList
showsVerticalScrollIndicator={false}
keyExtractor={keyExtractor}
data={data}
renderItem={renderItem}
/>
</View>
);
}
useCallback
以上方法,都是从输入框的入手,阻止他触发 Home 的更新,但有时候不得不更新 Home,这时就可以从 flatList 入手,阻止 flatList 重新渲染。用 useCallback 钩子函数包装 renderItem 和 keyExtractor,以确保它们只在 data 状态改变时重新计算。
const renderItem = useCallback(({item}) => {
console.log('renderItem');
return (
<Text style={styles.text}>{item.name}</Text>
);
}, []);
const keyExtractor = useCallback((item, index) => {
return item.id;
}, []);
React.memo
把 flatList 组件提取到一个组件(MyText),并用 React.memo 进行优化。虽然依旧会打印 renderItem,但 MyText 里的打印没有执行。
MyText 与 Home 平级
const MyText = memo(({item}) => {
console.log('MyText');
return (
<Text style={styles.text}>{item.name}</Text>
);
});
const renderItem = ({item}) => {
console.log('renderItem');
return (
<MyText item={item}/>
);
};
留言