一、前言
那一天我二十一岁,在我一生的黄金时代,我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云,我觉得自己会永远生猛下去,什么也锤不了我。
<p align="right">《黄金时代》王小波</p>
二、关于Hooks
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。Hook 的本质还是 JavaScript 函数,但是在使用它时需要遵循两条规则
只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook,确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。
只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook,你可以:
- 在 React 的函数组件中调用 Hook
- 在自定义 Hook 中调用其他 Hook
三、Hooks API
hooks的api有如下几个,按照官方文档划分了一下。
useState
这个算是最简单的 hook 函数了,它的作用就是初始化一个值,返回一个 和 initialState
相同的 state
,以及更新 state
的函数 setState
。
const [state, setState] = useState(initialState);
我们来使用一下 useState
import React, { useState } from 'react';
const Index = () => {
// setNumber 更新number函数
const [number, setNumber] = useState(0);
return (
<>
<div>{number}</div>
<button onClick={() => setNumber(number + 1)}>Add Number</button>
<button onClick={() => setNumber(number - 1)}>Sub Number</button>
</>
);
};
export default Index;
我们可以看到 useState
初始化一个值 number
为 0 之后返回一个数组,里面包含 number
本身 和 改变 number
值的函数 setNumber
我们通过两个按钮来绑定 setNumber
来改变 number
。实现 number
值的加一减一。
useEffect
useEffect
就是用来在函数组件中模拟类组件中的生命周期函数的。接受两个参数,第一个参数就是一个箭头函数。箭头函数返回一个函数,这个函数就是组件卸载的时候触发。(这里举个🌰:我们要监听页面的 message
的时候,那么就在 render dom 的后面监听,在 dom remove 的时候移除监听就行了)
关于第二个参数[deps]
- 没有第二个参数的时候,当
number
发生变化的时候,组件会 remove 后 render,有点像shouldComponentUpdate
生命周期。 - 当第二个参数为 [] 的时候,只会执行初始化的 render, 这个时候相当于生命周期
componentDidMount
- 当第二个参数不为空的时候,只有存在数组中的参数发生改变的时候,就会 remove 后 render,就是更新组件。
import React, { useEffect, useState } from 'react';
const Index = () => {
// setNumber 更新number函数
const [number, setNumber] = useState(0);
useEffect(() => {
console.log('render dom');
return () => {
console.log('dom remove');
};
}, [number]);
return (
<>
<div>{number}</div>
<button onClick={() => setNumber(number + 1)}>Add Number</button>
<button onClick={() => setNumber(number - 1)}>Sub Number</button>
</>
);
};
export default Index;
useContext
useContext
接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value prop 决定。其实这里的 useContext
就和 context.Consumer
的效果差不多了。这里提一下 context.Consumer
的用法
<MyContext.Consumer>
{value => {/* 基于 context 值进行渲染 */}}
</MyContext.Consumer>
看个🌰,定义了一个主题对象,里面有两个主题 light 和 dark,我们用 ThemeContext
接收一个context 对象, 使用 const theme = useContext(ThemeContext);
去订阅它,而返回的 theme
值是距离当前组件最近的 <ThemeContext.Provider value={themes.dark}>
的value prop。所以下面渲染的theme就是 dark主题。
import React, { useContext } from 'react';
const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
};
const ThemeContext = React.createContext();
function Index() {
return (
// 这里传入light主题
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return <ThemedButton />;
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
export default Index;
useReducer
useReducer
useState 的替代方案,它接收一个形如 (state, action) => newState 的 reducer,(和 redux 一样)在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值以下是用 reducer 重写 useState 一节的计数器示例。
import React, { useReducer } from 'react';
function reducer(state, action) {
console.log(state, action);
switch (action.type) {
case 'add':
return { count: state.count + 1 };
case 'sub':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Index() {
// return <h1>1</h1>;
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<h1>count:{state.count}</h1>
<button onClick={() => dispatch({ type: 'add' })}>+</button>
<button onClick={() => dispatch({ type: 'sub' })}>-</button>
</>
);
}
export default Index;
useCallback
useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
。可以看出它是用来缓存函数的,下面是我mentor在reviewcode的时候提出的优化建议 (标注cr的)
const TreeDemo = () => {
// cr: 函数作为子组件的props时,得考虑性能,用useCallback;
// const onSelect = (selectedKeys) => {
// console.log('selected', selectedKeys);
// };
// const onCheck = (checkedKeys) => {
// console.log('onCheck', checkedKeys);
// };
//reviewcode
const onSelect = useCallback(selectedKeys => {
console.log('selected', selectedKeys);
});
const onCheck = useCallback(checkedKeys => {
console.log('onCheck', checkedKeys);
});
// cr: 默认值用状态去控制,如果有外部传入的值,优先使用外部的值
return (
<Tree
checkable
//默认展开指定的树节点
defaultExpandedKeys={['0-0', '0-0-0', '0-0-1']}
//默认选中的树节点
defaultSelectedKeys={['0-0']}
//默认选中复选框的树节点
defaultCheckedKeys={['0-0-1', '0-0-0-1']}
onSelect={onSelect}
onCheck={onCheck}
treeData={treeData}
/>
);
};
useMemo
useMemo
用来缓存数据,实例如下 sum是个需要计算的参数,当我们没使用memo做缓存的时候,在更新rank的时候也会重新计算 sum的值,这显然是不合理的,我们只需要在更新count的时候才会去重新计算sum的值。这种优化有助于避免在每次渲染时都进行高开销的计算。
import React, { useMemo, useState } from 'react';
function Index() {
const [count, setCount] = useState(100);
const [rank, setRank] = useState(1);
let sum = useMemo(() => {
console.log('calculate');
let sum = 0;
for (let i = 0; i <= count; i++) {
sum += i;
}
return sum;
}, [count]);
return (
<>
<h1>{rank}</h1>
<button onClick={() => setRank(rank + 1)}>Add Rank</button>
<h5>{sum}</h5>
<button onClick={() => setCount(count + 1)}>Add Count</button>
</>
);
}
export default Index;
useRef
useRef 用来获取元素DOM节点,这里还有其他的方法
- 直接在节点中获取ref={node => this.node = node}
- 使用React.createRef()API
import React, { useRef } from 'react';
function Index() {
const node = useRef(null);
const getNode = () => {
// 打印节点
console.log(node.current);
};
return (
<div>
{/* 注意这里要绑定 */}
<h1 ref={node}>Node</h1>
<button onClick={() => getNode()}>Get Node</button>
</div>
);
}
export default Index;
useImperativeHandle
useImperativeHandle
可以让你在使用 ref 时自定义暴露给父组件的实例值,下面我们看个父组件调用子组件的focus函数
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
function Input(props, ref) {
console.log(props, ref);
const inputRef = useRef(null);
useImperativeHandle(
ref,
() => {
const handleFn = {
focus() {
inputRef.current.focus();
},
};
return handleFn;
},
[]
);
return <input type='text' ref={inputRef} />;
}
const UseInput = forwardRef(Input);
function Index() {
const current = useRef(null);
// console.log(current);
const handleClick = () => {
console.log(current);
const { focus } = current.current;
focus();
};
return (
<div>
<UseInput ref={current} />
<button onClick={handleClick}>获取焦点</button>
</div>
);
}
export default Index;
在上述的示例中,React 会将 <UseInput ref={current}>
元素的 current 作为第二个参数传递给 React.forwardRef
函数中的渲染函数 Input。Input会将 current 传递给 useImperativeHandle
的第一个参数 ref。注意:这里就没将current传递给给<input>
了,而是用了inputRef
来取<input>
的DOM实例在useImperativeHandle
里面操作,我的理解就是相当于官方不推荐让我们直接操作 input
。 而是使用了 useImperativeHandle
来代理操作而已。由 useImperativeHandle
暴露出方法供我们使用。
useLayoutEffect
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。也就是说useLayoutEffect回调执行在浏览器绘制之前,也就是 useLayoutEffect
回调函数代码可能会阻止浏览器渲染。
import React, { useEffect, useLayoutEffect, useState } from 'react';
function Index() {
const [color, setColor] = useState(0);
useLayoutEffect(() => {
console.log('render');
if (color === 0) {
setColor(color + 1);
}
}, [color]);
const colorStyle = {
color: color ? 'yellow' : 'red',
};
return (
<>
<div style={colorStyle}>color text {color}</div>
<button onClick={() => setColor(0)}>Click</button>
</>
);
}
export default Index;
从例子我们可以看到,当点击Click按钮的时候,视图会一直显示黄色,因为useLayoutEffect回调阻塞了浏览器的渲染。只有当回调代码执行完了之后,也就是setColor(color + 1)执行后,浏览器才会渲染,看到的视图也就一直是黄色了,当我们把useLayoutEffect换成useEffect的时候,我们快速点击Click按钮的时候就会出现闪烁现象,这是因为useEffect不会阻塞浏览器的渲染,点击按钮执行 serColor(0)后,视图就直接更新了变为红色。然后useEffect回调执行setColor(color + 1)之后,再变为黄色,所以就会出现闪烁现象。
useDebugValue
useDebugValue(value)
useDebugValue
可用于在 React 开发者工具中显示自定义 hook 的标签
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ...
// 在开发者工具中的这个 Hook 旁边显示标签
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
提示
我们不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。
useTransition
这个API处于试验阶段,等正式更新后再打算。