大概不久就是各种 hooks 漫天飞舞的世界。
本文记录一下 React Hooks 学习,算是还一下技术债。只是一些形式化的理解的整理,不求甚解。
State Hooks
import { useState } from 'react';
function ExampleWithManyStates() {
// 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
const [name, setName] = useState(() => 'jeremy'); // 允许传一个函数来初始化状态
return <Component {/* ...props */} />
}
useState()
方法就是管理状态state
的一个钩子。每次都能在库存
中钩出一对值和改值器,即一个状态值,一个修改该状态值的方法。
想象一下,如果 Hooks 机制能在ExampleWithManyStates
组件调用之外做好状态的持久化,那么不难理解每次update
该组件(即重新调用该组件函数),状态数据能够累计或递增维持了。
有木有很像我们以前写的辛辛苦苦写的一大堆的 reducer
?reducer
有一个初始状态 initState
和更改对应状态的 update
方法。初始状态或者update
后的状态是维护在一个全局的store
中的,这个store
就是起到一个全局状态持久化的作用。
将上面切片式的取状态的写法,做一下退化还原,退化成之前的 class
组件。
import { useState } from 'react';
class ExampleWithManyStates() {
constructor() {
// 声明一个 state 变量
this.state = {
age: 42,
fruit: 'banana',
todos: [{ text: '学习 Hook' }]
}
}
setAge(age) {
this.setState({age: age})
}
setFruit(fruit) {
this.setState({fruit: fruit})
}
addTodo(todo) {
this.setState({todos: [...this.state.todos, todo]})
}
deleteTodo(index) {
const {todos} = this.state;
this.setState({
todos: [...todos.slice(0, index), ...todos.slice(index+1)]
})
}
// .... other codes
}
写类似setAge(age) {this.setState({age: age}) }
这样的代码相当乏味,而且在class
组件状态很多的时候,无可避免的重复着既乏味又占代码的写法(当然这种简洁的钩子写法没有之前,只会心里莫名犯嘀咕,知道哪里不对劲也使不上劲儿)。
对比新旧组件写法,不难发现2点(实际上只是一点 / 🤦♀️):
1、通过 useState()
这样的钩子,React 将这些样板代码揉进自己的框架之中,减少了用户样板代码冗余。
2、代码更紧凑了,用一个钩子方法,就将这些class
实例方法全部拍平到一个函数中。
函数组件被多处复用,其状态如何持久化?
那么更多的问题来了,对于一个函数组件,若是被包裹了一次,或许不难理解在函数调用之外,用缓存这个持久化方案解决其函数内部状态问题,这个例子的这一段:手动模拟更新还原过程 写的很清楚。但是若这个组件被多处复用,又该如何做对应位置的状态持久化呢?
// sorry todo
useEffect
数据获取request
,设置订阅(事件监听)、手动更改 React 组件中的 DOM 都属于副作用。但是不是一定所有副作用非得放在useEffect
钩子里执行,不一定,但可能不是一个好习惯~
先不纠结什么是副作用,从形式上,可以把 useEffect Hook
看做是 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。useEffect
中的方法,是在 render
完 DOM
后再执行的,好比于class
组件中执行完render
后再执行componentDidMount
方法一样。
import React, { useEffect, useState} from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Clicked ${count} times`;
console.log('2');
return () => {
// 下次组件被更新,则此尾调会在`2`之前先执行,表示一次清除动作
console.log('3');
}
});
console.log('1');
return (
<div>
<p>clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Counter;
以上代码更像是componentDidUpdate
,因为useEffect
里的方法,会在组件第一次渲染之后和每次组件更新都会执行一次。
小心设置依赖数组
很多时候,我们只需执行一次,即等同于componentDidMount
;也有时候,需要有条件的执行,怎么办呢?汇总一下。
1、仅执行一次。给useEffect
多传一个空数组[]
,比如:
useEffect(() => {
document.title = `Clicked ${count} times`;
}, []);
2、选择性的执行。给useEffect
多传一个[count]
,比如:
useEffect(() => {
document.title = `Clicked ${count} times`;
}, [count]);
数组参数前面的方法,是否执行,依赖于数组的值前后两次是否变化。
3、每次刷新都执行一遍。不传任何参数,比如:
useEffect(() => {
document.title = `Clicked ${count} times`;
});
知道了传与不传的区别,但知道传什么似乎更重要。又有两点须知:
1、每次刷新完毕后,都会执行useEffect
方法。而useEffect
每次执行都会传入一个新的匿名函数,保证了函数中所需变量都是新的,不受闭包影响。所以useEffect
的第一个参数方法中,直接找外部拿参数变量即可。
2、useEffect
依赖数组中的值,并不会作为前面函数的参数空间。
但到底该传递什么依赖呢?插播一段 官方说明。
只有 当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state
}, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` 没有被指定为依赖
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state
}, 1000);
return () => clearInterval(id);
}, [count]); // 🔴 Bug: useEffect 下一次执行总会先把定时器移除
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量
}, 1000);
return () => clearInterval(id);
}, []); // ✅ 我们的 effect 不适用组件作用域中的任何变量
return <h1>{count}</h1>;
}
shouldComponentUpdate
生命周期,该如何实现呢?请看我该如何实现 shouldComponentUpdate?。
componentWillReceiveProps
生命周期,该如何实现呢?
useContext
借助React.createContext 和 useContext()
,我们拥有了一种 “透传” 的的能力,能将顶层的属性,一次传递到任意子层级的组件,而不需要层层接力式的传递。
这个钩子,在处理语言intl
属性时,简直不能太方便。煮一个官方的栗子:
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Father>
<Child />
</Father>
</ThemeContext.Provider>
);
}
const Child = (props) => {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useCallback
function Foo(props) {
const [count, setCount] = useState(0);
return <Button onClick={() => props.handleVisible(false)}>Click Me</Button>;
}
// 优化一下
function Foo(props) {
const [count, setCount] = useState(0);
// 这样只创建一次回调函数
const handleVisible = useCallback(e => {
props.handleVisible(false);
}, [])
return <Button onClick={handleVisible}>Click Me</Button>;
}
//
function Foo(props) {
const [count, setCount] = useState(0);
const reportCount = useCallback( count => e => {
props.reportCount(count);
},)
return <Button onClick={reportCount(count)}>Click Me</Button>;
}
useMemo
useMemo
能记忆一个方法执行的结果值,假如下次刷新组件时,依赖不变,则useMemo
不会执行这个方法,而是直接拿到上次记忆的值。
如果这个方法是个耗时运算,或是返回一个组件,当依赖不变,就直接拿记忆值,这样就能起到性能优化的效果。
const long_time_result = useMemo(() => {
const result = long_time_function(count);
return result;
}, [count])
useRef
是用对象引用方式,用户代码可以用它来做一般数据的缓存。说白了还是一种持久化。
so 和useState
有什么差别呢?唯一差别是:useState
多返回了一个可以刷新本组件的方法,而useRef
单纯提供一个引用访问链,你组件再怎么重复刷新,这个引用链也不会断掉。
这个钩子,在组件两次刷新之间,因为会产生两帧独立的闭包,所以用它来保持数据关联性就非常合适了。
React.memo
function MyComponent(props) {
// render using props
}
function areEqual(prevProps, nextProps) {
// return true if passing nextProps to render would return
// the same result as passing prevProps to render,
// otherwise return false
}
export default React.memo(MyComponent, areEqual);
总结一下
1、useState
和 useRef
钩子行为相似。
2、useContext
具有透传能力
3、其他钩子在于依赖。
4、捕获值的这个特性是我们写钩子最最需要注意的问题,它是函数特有的一种特性,并非函数式组件专有。函数的每一次调用,会产生一个属于那一次调用的作用域,不同的作用域之间不受影响。
其他
react-redux的钩子
状态管理方面,React 社区最有名的工具当然是 Redux。在 react-redux@7.1 中新引用了三个 API:
- useSelector。它有点像 connect() 函数的第一个参数 mapStateToProps,把数据从 state 中取出来;
- useStore 。返回 store 本身;
- useDispatch。返回 store.dispatch。
关于测试
觉得还是到改了一部分 hooks 写法后,在加单元测试。现在堆积了很多逻辑的 class
组件真心难写。如何测试使用了 Hook 的组件4
网友评论