记录一下 React Hooks
大概不久就是各种 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
}
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 (
);
}
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
);
}
const Child = (props) => {
const theme = useContext(ThemeContext);
return (
);
}
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}
>
);
}
useCallback
function Foo(props) {
const [count, setCount] = useState(0);
return ;
}
// 优化一下
function Foo(props) {
const [count, setCount] = useState(0);
// 这样只创建一次回调函数
const handleVisible = useCallback(e => {
props.handleVisible(false);
}, [])
return ;
}
//
function Foo(props) {
const [count, setCount] = useState(0);
const reportCount = useCallback( count => e => {
props.reportCount(count);
},)
return ;
}
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
clicked {count} times
发表评论 (审核通过后显示评论):