what is?
Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class。
Hooks是React16.8的新特性。能让你不必写class,也能用React的特性。
why we need?
简单说说为什么要搞出Hooks这个东西。
官方的解释是为了克服Class组件存在的三点弊端:
- 有状态的逻辑难以复用(It’s hard to reuse stateful logic between components)
项目中公共的逻辑并没有一个很好的复用方式。复用逻辑在此之前的方案中可选高阶组件或者render props。但这些方案并不完美,一方面,引入了毫无意义的DOM节点,另一方面,组件可能在改写的时候重构量很大。
通过Hooks,就可以从组件中提取出有状态的逻辑,这样它还能独立被测试和重用。Hooks可以使你不用改变组件层级就能复用有状态逻辑。这点主要表现在自定义Hooks的使用中。
- 复杂的组件不好理解(Complex components become hard to understand)
我们经常会维护那些起初简单,但是最终变成一堆无法管理的状态逻辑和副作用的组件。每一个生命周期方法通常包含了不相关的逻辑。、
比如说,在componentDidMount和componentDidUpdate中,组件发起数据请求。但是,这个相同的componentDidMount也可能包含了一些没有联系的逻辑,比如设置监听器,而这些又要在componentWillUnmount中被移除。相互联系的代码更改后被分开,但是毫无关联的代码却在同一个方法中。
useEffect的各种用法解决了这个问题。
- Class足够迷惑(Classes confuse both people and machines)
这里Facebook官方认为,class是学习React的巨大障碍(eg:你得记得“bind”事件处理器。没有不稳定的语法建议,代码将十分冗长。人们很容易理解state,props,自顶向下数据流,却对class感到挣扎。函数组件和class组件的区别以及应什么时候来使用它们也让有经验的React开发者们争论不休)
另外,概念上讲,Hooks使得React也更加拥抱函数。
how to be used?
基本的Hook
useState
这一定是学习接触的第一个Hook。
核心就是赋予函数组件以状态。Hook之所以叫钩子,可以理解成,把一些React能力钩进当前的函数组件(比如状态)。
const [state, setState] = useState(initialState);
useState调用后的返回值是一个tuple元组。包含了当前的值以及设置此值的函数—setState(当然,这个函数名随意定义,函数变量引用而已)。
函数调用和class组件中this.setState类似有两种形式:1、setState(newValue); 2、 setState(prevValue=>(newValue))。同理,每次的当新的值依赖于旧值计算而得出时,推荐使用第二种形式;(原因参考)
useEffect
这不一定,但很可能是很多人接触的最后一个Hook。(因为后面的旧不看了)
不过它确实很重要,还有点麻烦。
useEffect看名字即知道这个东西要处理副作用了。React官方讲的副作用指的是一些,修改DOM、网络请求、定时器等操作。
useEffect(didUpdate, condition);
这里didUpdate是一个effect,确切的说,是一个函数。如果函数有返回值,返回值必须是一个没有返回值的函数。这个函数执行的是卸载任务。后文展开介绍。
这里condition是出发这个effect的执行条件,是一个数组。数组中的值变化,在那一轮渲染的结束就会触发一波effect的执行。
结合参考下在@types/react中index.d.ts中的定义更好理解参数的形式:
// useEffect定义
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
// 其参数1定义
type EffectCallback = () => (void | (() => void | undefined));
// 其参数2定义
type DependencyList = ReadonlyArray<any>;
问题1 :执行时机
这里执行时机涉及两个:一个是effect的执行时机,一个是effect返回的函数,用来做清理的那个的执行时机。effect的执行时机很明确,就是在每次渲染结束后,所谓 ‘didUpdate’ 。
但是其返回的函数的执行时机我自己之前理解有偏差,我以为是组件卸载时,因为这个返回的函数的目的是用来做卸载的。当然,这也不完全是错的,假设这个组件只经历了一次渲染就被卸载了,那这个返回的函数确实是卸载前被执行。但是,如果函数组件需要多次被渲染时,这个返回的函数的执行时机是:(官方: previous effect is cleaned up before executing the next effect.在执行下一个 effect 之前,上一个 effect 就已被清除)就是说,这个返回的函数在effect执行之前被调用。
**
问题2: 关于执行时机,新的问题来了。
这段代码在第一次点击按钮后输出神马🐎?
type IProps = {};
const CaptureValue: React.FC<IProps> = (props) => {
const [stateValue, setStateValue] = useState(0);
useEffect(() => {
console.log('did update', stateValue);
return () => {
console.log('should clean', stateValue);
}
})
console.log('rendering', stateValue );
return (
<div>
<button onClick={()=>{setStateValue(v=>v+1)}}> ABCDEFG</button>
</div>
);
};
控制台输出的是?
rendering 1
should clean 0
did update 1
还是?
rendering 1
should clean 1
did update 1
后文Capture Value中回答….
问题3: 利用useEffect的第二个参数,模拟class生命周期
这个问题倒是相对简单了:
第二个参数传空数组,模拟componentDidMount;
第二个参数传非空数组,比如[X], 模拟componentDidUpdate(prevProps){ if(prev.X !== this.props.X){ effect }};
第一个参数返回函数,模拟componentWillUmount;
这里模拟这个词语并不是太好,使用Hooks本可以不和class组件做类比,所以,甭管模拟什么生命周期了,记住调用时机就够了。
**
useContext
const value = useContext(MyContext);
useContext戏路较窄。
它接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的
当组件上层最近的
需要注意的是,参数是Context对象本身而不是MyContext.Consumer。
内置的其他Hook
useReducer
官方认为这是useState的替代方案,但却是有点像Redux的替代方案!
看例子就全明白了:
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>
</>
);
}
以前有句话说:如果你不知道你的React项目项目需要不需要使用Redux,那么答案就是不需要!
没错,你还可以局部使用useReducer。
useCallback
官方有讲:useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
useMemo
这两个钩子类似的功能,同时也和useEffect有相似之处。先看下例子。
const TestMemo: React.FC<Props> = (props) => {
const [a, setA] = React.useState(0);
const [nbr, setNbr] = React.useState(0);
const res = React.useMemo(()=>{
console.log('triggered ...');
return 'changed' + new Date().getTime();
}, [a]);
return (
<div>
<p> result is {res} </p>
<button onClick={()=>{setA(a=>a+1)}}> click add a</button>
<button onClick={()=>{setNbr(nbr=>nbr+1)}}> click add nbr</button>
</div>
);
};
和useEffect的第二个参数deps类似,当deps发生变化时,useMemo会在渲染阶段(不是渲染完成后)调用其函数,函数的返回值就在当前渲染过程中参与UI的计算。但是,和useEffect不同之处在于,函数里面的过程和返回不能涉及副作用。
这是一种性能优化的手段,目的是为了填补class组件中的shouldComponentUpdate方法能做的性能优化工作。正如上面例子中,不是所有触发更新的状态或者属性的变化,我都想更新组件或者子组件。
再看一个useCallback的例子:
function Parent() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
return doSth(count);
}, [count]);
return <div>
<h4>{count}</h4>
<Child callback={callback}/>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</div>;
}
function Child({ callback }) {
const [count, setCount] = useState(() => callback());
useEffect(() => {
setCount(callback());
}, [callback]);
return <div>
{count}
</div>
}
若这里没有使用useCallback,我们把doth(count)的计算结果用值传给Child组件,当然,功能是一样的。但,那么假如和count不相干的value变化了,父组件触发了更新,这个结果
useRef
和React.createRef()的用法几乎一致。
const refContainer = useRef(initialValue);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象引用在组件的整个生命周期内保持不变。不变,很重要。
useImperativeHandle
这个Hooks通常和ForwardRef一起使用。用来对上层暴露组件内部的方法。(ForwardRef可以将父组件ref引用向下传递给子组件,处理高阶组件常用一些)。useImperativeHandle其实就是给第一个参数的ref对象,添加第二参数的是一个函数返回的对象的所有属性。
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
在本例中,渲染
这个通常在使用antd.Form的时候可能会用到。
另外,官方的useLayoutEffect、 useDebugValue没了解过。
useLayoutEffect
useLayoutEffect并不是没有用。
参考:https://reactjs.org/docs/hooks-reference.html#useimperativehandle
作用可以简单理解和useEffect像,但是核心区别是渲染时机不同:
useEffect 在渲染时是异步执行,并且要等到浏览器将所有变化渲染到屏幕后才会被执行。 useLayoutEffect 在渲染时是同步执行,其执行时机与 componentDidMount,componentDidUpdate 一致
自定义Hook
自定义Hook才是Hook的精髓。
上文中说过,class组件中存在一个问题是,我们很难把那些可复用的逻辑(纯逻辑),用一种完全UI不相关的方式独立出来。
比如我项目中有很多地方使用了同一功能,比如请求某接口。但是我期望在不同页面中用不同的UI展示,比如A页面,该接口返回的数据我用文本,B页面我想用表格。
当然,最为糟糕的解决方案是,将他们封装成很多组件。。。更好一点的思想是,我用高阶组件,把等着用不同UI渲染的组件用参数的形式传进去。确实好了很多,至少算是复用了逻辑。
但是问题还是存在,比如,增加了无意义的节点。另外,高阶组件通常比较复杂,很难阅读理解,后期扩展和维护的时候,可能要深入查看组件内部。
这时候自定义Hook的价值就体现出来了。就目前复用逻辑的三种方案里面(others are Hoc & render props),自定义Hook确实是最好的。
简单理解下自定义Hook,就是官方提供了一种机制,让你自己书写hook(use开头的函数就被认为是自定义hook)。利用这种机制、原生hook的各种特性来抽象出一些可复用的东西。这样就像可插拔,即用即拿的灵活性较之前提升了太多。当然,不强求一定要把UI和逻辑解耦。
官方文档Building Your Own Hooks足够详细,从Hoc到自定义Hook,看完会觉得很惊艳。有意思的是,这篇文档的最后一节叫useYourImagination,一方面遵循自定义hook的命名规范,有一方面含义深远—你的想象有多远,hook就能触及多远~
官方强调的两个注意点
段落就直接翻译了Rules of Hooks
只能在顶层调用Hooks
不要在循环,条件,或者嵌套的函数中调用Hooks。反之,必须在React函数的顶层使用Hooks。遵循这条规则,你就能保证在每次组件渲染中Hooks以相同的顺序调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。(如果你对此感到好奇,我们在后文中会有更深入的解释。)
只能再React函数中调用Hooks
别在普通的JavaScript函数中调用Hooks。反之,你可以:
- 在React函数组件中使用;
- 在自定义Hooks中使用(下一讲我们就学习);
其他补充
Capture Value特性
在上文useEffect时,提到了Capture Value。
当你在使用hook发现自己拿到的状态不是最新的,You May Ask:Why am I seeing stale props or state inside my function?
这个特性可以简单的概括下:
每次render 都有自己的事件处理、自己的state和props、以及effects。
细品。。。
每次渲染自己的state和props
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
每次点击导致了一次次渲染,但是渲染的是state的值,是将值传入进行渲染。
都有自己的事件处理
const App = () => {
const [temp, setTemp] = React.useState(5);
const log = () => {
setTimeout(() => {
console.log("3 秒前 temp = 5,现在 temp =", temp);
}, 3000);
};
return (
<div
onClick={() => {
log();
setTemp(3);
// 3 秒前 temp = 5,现在 temp = 5
}}
>
xyz
</div>
);
};
这里在触发log函数中定时器的那一次渲染,temp的值是5。所以,这个按钮的事件处理是属于“那一次的”。
- 每次render都有自己的effect
所以上文中的例子
type IProps = {};
const CaptureValue: React.FC<IProps> = (props) => {
const [stateValue, setStateValue] = useState(0);
useEffect(() => {
console.log('did update', stateValue);
return () => {
console.log('should clean', stateValue);
}
})
console.log('rendering', stateValue );
return (
<div>
<button onClick={()=>{setStateValue(v=>v+1)}}> ABCDEFG</button>
</div>
);
};
点击后输出应该是:
rendering 1
should clean 0 // 这个‘卸载’卸载的是上一次的。
did update 1
Capture Value如何绕过?
利用 useRef 就可以绕过 Capture Value 的特性。
这是useRef比cerateRef强大的一点,它不只能持有DOM节点或者ReactNode的引用。
可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离。
function Example() {
const [count, setCount] = useState(0);
// 把值的引用用ref保存了
const latestCount = useRef(count);
useEffect(() => {
// Set the mutable latest value
latestCount.current = count;
setTimeout(() => {
// Read the mutable latest value
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
// ...
}