一、useState
useState传参可有两种方式:
- 直接传入初始值:
React.useState(0)(常用) - 传一个返回初始值的函数:
React.useState(()=>0)
setState用法
(1)数据为对象
若使用对象作为state,需注意:
setState时必须传入新对象,否则不会更新setState不会自动合并未变的属性
【示例】
const [state, setState] = React.useState({a:1, b:2})
只修改属性a:
setState({
...state,
a: 2
})
(2)传函数作实参
setState的参数也可以是函数,优先使用这种方法,可以避免连续 setState 时失败
setState(x=>x+1)
setState(x=>x+1)
二、useReducer
useReducer可看作是useState的复杂版,它可以将对某个数据进行的所有操作封装在一起,简化修改数据时的代码。
首先,创建数据的操作函数reducer:
const reducer = (state, action){
//...
}
然后useReducer(按顺序传入reducer和初始值):
const [state, dispatch] = React.useReducer(reducer, initialValue)
修改数据使用dispatch函数:
dispatch(/* 实参 */)
dispatch会调用reducer,其参数会成为reducer的第二个参数action,旧state为第一个参数,reducer的返回值将成为新的state,并触发更新。
1、案例
useReducer的使用通常按如下四步:
- 创建初始值
initialState - 创建所有操作
reducer(state, action){/* ... */} - 调用
useReducer(reducer, initialState),获得读和写的API - 修改数据的传参形式:
(type: '操作类型')
【例】封装对n的加、减操作
创建initialState、reducer:
const initialState = {
n:1
}
const reducer(state, action){ //action包含对n的操作类型的信息,如{type:'add', number: 1}
if(action.type === 'add'){
return {n: state.n + action.number}
}else if(action.type === 'minus'){
return {n: state.n - action.number}
}else{
throw new Error('unknown type')
}
}
调用useReducer:
const [n, dispatch] = React.useReducer(reducer, initialState)
修改n:
dispatch({type:'add', number:1}) // n + 1
dispatch({type:'minus', number:2}) // n - 2
2、用useReducer代替Redux
通常的实现步骤:
1、将数据集中在一个 store 对象
const store = {/* ... */}
2、将操作集中在 reducer
function reducer(state, action){/* ... */}
3、创建一个 Context
const Context = React.createContext(null)
4、App 组件内创建数据读写API
const [state, dispatch] = useReducer(reducer, store)
5、读写API传给 Context
<Context.Provider value={{state, dispatch}}>
...
</Context.Provider>
6、子组件获取读写 API
const {state, dispatch} = useContext(Context)
三、useContext
上下文可以实现在一个范围内,将组件的数据共享给后代组件,可视为是局部的全局变量(仅在某个范围内可用)。
【用法步骤】
- 创建上下文:
const C = createContext(null) - 用
<C.Provider value={/* 共享的数据 */}>圈定作用域 - 作用域内用
useContext(C)使用上下文
【例】
const C = createContext(null)
function App(props){
const [n, setN] = useState(1)
return (
<C.Provider value={{n, setN}}>
<Son />
</C.Provider>
)
}
function Son(props){
const {n, setN} = useContext(C)
return (
/* 可以使用n和setN */
)
}
四、useEffect
React渲染一个组件的大致流程:
App() ----> 得到虚拟DOM ----> 真DOM ----> 渲染引擎渲染页面
useEffect会在渲染引擎渲染完成后执行
五、useLayoutEffect
useLayoutEffect在渲染引擎工作前(上述第三、四步之间)执行。
useLayoutEffect总是比useEffect先执行,为了用户体验,应优先用useEffect(即保证优先渲染)。
六、useMemo
1、memo
考虑以下情景:
App 组件使用了子组件 Son,当父组件更新时,即便组件没变,也会再次调用 Son。
可以用React.memo来消除多余的子组件更新:
const Son = React.memo(/* Son */)
【问题】:
如果给子Son传入了一个函数,Son仍然会重新执行:
function App(props){
const fn = ()=>{}
return (
<Son onClick={fn} />
)
}
因为在App每次执行时,fn是不同的(不同的地址),所以会重新执行 Son。
这可以用useMemo解决。
2、useMemo
useMemo可以使对象缓存下来,即每次执行App时,对象不会改变
function App(props){
const fn = useMemo(()=>{
return /* 原fn */
}, [])
}
只有当数组中的数据变化时,才会生成新的fn,否则fn保持不变。
useMemo写法比较复杂,可以用useCallback代替。
3、useCallback
用法:
useCallback(fn, [n]) //等价于:
useMemo(()=>fn, [n])
七、useRef
useRef得到一个对象,这个对象在每次重新执行 App 时是不变的(同一个地址)。
const count = useRef(0)
然后使用count.current读取值。
修改count.current不会触发更新。
1、forwardRef
函数组件不能传递 ref 属性(props不包含ref):
function App(props){
const ref = useRef()
return (
<Son ref={ref} />
)
}
// error
用forwardRef对Son进行封装,就可以接收ref了:
const Son = forwardRef((props, ref)=>{
// ref参数收到来自App的ref
})
2、useImperativeHandle
实际上就相当于是 setRef,用于自定义 ref 的属性。
【例】
const Son = forwardRef((props, ref)=>{
useImperaitveHandle(ref, ()=>{
return /* 返回 ref 的新值 */
})
})
八、自定义Hook
自定义Hook,将对数据的操作进行封装,然后使用useXXX
九、Stale Closure -- 过时的闭包
React函数组件每次执行都会产生新的变量,这会导致即使重新渲染后,某些闭包函数依然使用了上一次函数组件执行时产生的变量,容易造成误解。