A simple demo about Hooks
先看一个有状态组件:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
我们再来看一下使用hooks后的版本:
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Hooks概览
- useState
- useEffect
- useContext
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
useState
initialState参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。
initialState也可以换成一个函数,在函数中计算并返回初始的 state。
const [state, setState] = useState(initialState);
// setState的参数可以是一个state值,也可以是一个函数。
setState(newState);
setState(prevState => ({...prevState, ...newState}))
useEffect
该 Hook 接收一个包含命令式、且可能有副作用代码的函数。
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM
、添加订阅
、设置定时器
、记录日志
以及执行其他包含副作用
的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
// 清除effect
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
},[]);
注意:
1)effect的执行时机,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。
这点与 componentDidMount
、componentDidUpdate
不同,这使得它适用于许多常见的副作用场景,比如如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新的操作。
2)effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建,即依赖调用。useEffect
的第二个参数就是依赖数组。如果传入空数组[],那么依赖不会触发第二次,也就是useEffect
的函数只会被执行一次。
useContext
接收一个 context 对象(React.createContext 的返回值)
import {createContext} from 'react';
const Context = createContext();
...
<Context.Provider value={value}>{children}</Context.Provider>
const value = useContext(Context);
当组件上层最近的 <Context.Provider>
更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。
useReducer
useState
的替代方案,接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
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: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
state状态的惰性初始化。需要将 init 函数作为 useReducer
的第三个参数传入,这样初始 state 将被设置为 init(initialArg)。
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'reset', payload: initialCount})}>Reset</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。)
useCallback
返回一个 memoized 回调函数。该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时会很有用。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个 memoized 值。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
useRef
// 调用方法
const refContainer = useRef(initialValue);
// 命令式地访问子组件
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
注意:当 ref 对象内容发生变化时,useRef 并不会通知你。
如果要在绑定dom元素后执行执行某些代码,可以使用ref回调。
测量一个dom元素的四至范围:
function MeasureExample() {
const [rect, ref] = useClientRect();
return (
<>
<h1 ref={ref}>Hello, world</h1>
{rect !== null &&
<h2>The above header is {Math.round(rect.height)}px tall</h2>
}
</>
);
}
function useClientRect() {
const [rect, setRect] = useState(null);
const ref = useCallback(node => {
if (node !== null) {
setRect(node.getBoundingClientRect());
}
}, []);
return [rect, ref];
}
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。
useLayoutEffect
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
自定义Hook
Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则:
- 只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。 - 只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。你可以:- 在 React 的函数组件中调用 Hook
- 在自定义 Hook 中调用其他 Hook。
我们发布了一个名为 eslint-plugin-react-hooks
的 ESLint 插件来强制执行这两条规则。
在两个组件中使用相同的 Hook 会共享 state 吗?不会。
网友评论