Hook 产生的动机
- 代码层次更清晰;在类组件中,共属于一个功能的代码分散在各个钩子函数中;
- 更好的状态逻辑复用,renderProps 和高阶组件不是那么好用;
- 干掉 this。
Hook 使用规则
- 只能在最外层调用 Hook;不要在循环、条件判断或者子函数中调用(Hook 靠的是调用顺序来确定哪个 state 对应哪个 useState);
- 只在函数组件中调用 Hook。
useState
state 只在组件首次渲染的时候被创建;在下一次重新渲染时,useState 返回给我们当前的 state。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state。
1
|
const [state, setState] = useState(() => someExpensiveComputation(props));
|
在初始渲染期间,state 与传入的 initialState 值相同;setState 函数用于更新 state,它接收一个新的 state 值并将组件的一次重新渲染加入队列。
1
2
3
4
5
6
7
8
9
10
11
12
|
function Counter({ initialCount }) {
// initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略
const [count, setCount] = useState(initialCount);
return (
<>
<span>{count}</span>
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
|
与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。
1
2
3
|
setState(prevState => {
return { ...prevState, ...updatedState };
});
|
注意:在组件重新渲染时,state 和 setState 不会重新初始化(渲染前后都是同一个引用),state 像保存在 ref 中的变量一样。
useEffect
useEffect 会在浏览器完成本次渲染之后进行下次渲染之前调用,不会阻塞浏览器更新屏幕;每次运行 effect 的时候,DOM 都已经更新完毕。某种意义上讲,effect 更像是渲染结果的一部分,每个 effect“属于”某一次特定的渲染。
1
2
3
4
5
6
|
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `u clicked ${count} times`;
});
}
|
默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它在只有**某些值(能够触发组件重新渲染的值)**改变的时候才执行;如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组 [] 作为第二个参数,effect 内部的 props 和 state 就会一直持有其初始值。
副作用函数还可以通过返回一个函数来指定如何“清除”副作用;通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器等资源;要实现这一点,useEffect 函数需返回一个清除函数;如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。
1
2
3
4
5
6
7
8
9
10
11
12
|
useEffect(
() => {
// 添加订阅
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
},
// 只有当 props.source 改变后才会重新创建订阅
[props.source]
);
|
useContext
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值;当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider/> 的 value 决定。当组件上层最近的 <MyContext.Provider/> 更新时,该 Hook 会触发重渲染,并使用最新传递的 value 值。即使祖先使用 React.memo、React.PureComponent 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
const themes = {
light: {
foreground: '#000',
background: '#eee'
},
dark: {
foreground: '#fff',
background: '#222'
}
};
// 默认值为 themes.light
const ThemeContext = createContext(themes.light);
const App = () => (
// 传递值为 themes.dark
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
const Toolbar = props => (
<div>
<ThemedButton />
</div>
);
const ThemedButton = () => {
// 传递 createContext 的返回值
const theme = useContext(ThemeContext);
return (
// 当 theme 改变时,触发重新渲染
<button
style={{
background: theme.background,
color: theme.foreground
}}
>
噼里啪啦
</button>
);
};
|
useReducer
useState 的替代方案(useState 就是使用 useReducer 实现的);接收一个 reducer,并返回当前的 state 以及与其配套的 dispatch 方法;dispatch 不会在组件重新渲染的时候重新初始化,渲染前后的 dispatch 是同一个方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
const formInit = {
username: '',
useremail: ''
};
const formReducer = (state, action) => {
switch (action.type) {
case 'CHANGE':
const { name, value } = action.payload;
return { ...state, [name]: value };
case 'RESET':
return { ...formInit };
default:
return state;
}
};
const Form = () => {
const [formValues, dispatch] = useReducer(formReducer, formInit);
const handleChange = e => {
dispatch({
type: 'CHANGE',
payload: e.target
});
};
return (
<form>
<div>
<label htmlFor="username">UserName</label>
<input
type="text"
name="username"
value={formValues.username}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="useremail">UserEmail</label>
<input
type="email"
name="useremail"
value={formValues.useremail}
onChange={handleChange}
/>
</div>
<button>Submit</button>
<button onClick={() => dispatch('RESET')}>Reset</button>
</form>
);
};
|
注意:如果通过 createContext 将 useReducer 的 state 和 dispatch 当作 value 传递下去;你甚至可以得到一个简单的 redux。
useCallback & useMemo
Memo 是一种优化技术,大体上就是缓存昂贵计算的值并在相同输入的情况下直接返回缓存值;useCallback 用来缓存函数的引用,useMemo 用来缓存计算数据的引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/**
* 在 a 和 b 的变量值不变的情况下,即使组件重新渲染,memoCallback 的引用不变
* useCallback 的第一个入参函数会被缓存,从而达到渲染性能优化的目的
*/
const memoCallback = useCallback(() => {
// ...
doSomething(a, b);
// ...
}, [a, b]);
/**
* 在 a 和 b 的变量值不变的情况下,memoValue 的值/引用不变
* useMemo 函数的第一个入参函数不会被执行,从而达到节省计算量的目的
*/
const memoValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
|
具体使用场景:
- 当复合数据(函数也是一种)存在于 useEffect 的依赖数组中,同时当前 useEffect 中有触发组件重新渲染的逻辑;则需要将数据用 useMemo | useCallback 包裹,否则会导致组件无限循环渲染。(1 === 1,{} !=== {}) ;
- 父组件重新渲染会导致子组件重新渲染,当子组件使用 React.memo 包裹时(仅当 props 改变才会出触发重新渲染),如果复合数据不使用 useMemo | useCallback 包裹,会导致 React.memo 失效。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
// 获取上一次指定的值
const usePrevValue = value => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
const App = ({ name }) => {
const [count, setCount] = useState(0);
/**
* 每次组件重新渲染,handleCount 都是重新创建的新函数;
* 将 handleCount 作为 props 传递给其他组件时,
* 会导致 PureComponent、React.memo 等相关优化失效
*/
const handleCount = () => {
setCount(count => count + 1);
};
// useCallback 将传入的函数永久缓存了起来
const memoHandleCount = useCallback(() => {
setCount(count => count + 1);
}, []);
const prevHandleCount = usePrevValue(handleCount);
const memoPrevHandleCount = usePrevValue(memoHandleCount);
console.log(prevHandleCount === handleCount); // false
console.log(memoPrevHandleCount === memoHandleCount); // true
// 将性能消耗比较大的计算缓存起来,只有 name 改变时会重新计算,count 改变不会进行重新计算
const memoArray = useMomo(() => {
return Array(999)
.fill(name)
.map(v => v.toUpperCase());
}, [name]);
return (
<div>
<span>{name}</span>
<span>{count}</span>
<button onClick={handleCount}>AddOne</button>
</div>
);
};
|
注意:如果 useCallback/useMemo 缓存的函数/值不依赖组件内部的变量,可以将函数/值提到组件外部;酱紫,不使用 useCallback/useMemo 可以达到相同的效果。
useRef
useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数 initialValue;返回的 ref 对象在组件的整个生命周期内保持不变。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
const TextInputWithFocusButton = () => {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus</button>
</>
);
};
|
useRef 不仅用于 DOM refs,ref.current 是一个可以容纳任何值的通用容器,类似于 class 的实例属性;useRef 和自建一个 {current: …} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象(当然你可以在组件外定义一个变量,效果是一样的)。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
const Timer = () => {
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
// ...
});
return () => {
clearInterval(intervalRef.current);
};
});
// ...
return null;
};
|
useLayoutEffect
顾名思义,它是作用在浏览器 Layout 阶段的“副作用”;和 useEffect 的区别只是调用时机不同;既然是作用在 Layout 阶段的副作用,那么必须在浏览器渲染之前进行调用,所以可能会阻塞浏览器渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新,如果在里面执行耗时任务的话,页面就会卡顿。
自定义 Hook
自定义 Hook 是一个函数,其名称以“use”开头(方便静态代码检测),函数内部可以调用其他的 Hook;它是一种重用状态逻辑的机制,其中的所有 state 和副作用都是完全隔离的。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
|