ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] # 介绍 React 是主流的前端框架,v16.8 版本引入了全新的 API,叫做 [React Hooks](https://zh-hans.reactjs.org/docs/hooks-reference.html),颠覆了以前的用法。 如果你是 Hook 初学者,建议先阅读 [https://usehooks.com/](https://usehooks.com/) 以及 [Dan Abramov 的个人博客](https://overreacted.io/)。 axios 以及 immer 等库都未能 “幸免”,被 Hook 包裹了一层而变成了 [axios-hooks](https://github.com/simoneb/axios-hooks) 以及 [use-immer](https://github.com/immerjs/use-immer)。 # 思想 React 团队希望,组件不要变成复杂的容器,最好只是数据流的管道。开发者根据需要,组合管道即可。**组件的最佳写法应该是函数,而不是类。** React 的函数组件,有重大限制: 1. 必须是纯函数 2. 不能包含状态 3. 也不支持生命周期方法 4. 因此无法取代类(没有 this) **React Hooks 的设计目的,就是加强版函数组件,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码 "钩" 进来。** React Hooks 就是那些钩子。 # 钩子函数 React 为我们提供的钩子: * [Basic Hooks](https://zh-hans.reactjs.org/docs/hooks-reference.html#basic-hooks) * [`useState`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usestate) * [`useEffect`](https://zh-hans.reactjs.org/docs/hooks-reference.html#useeffect) * [`useContext`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usecontext) * [Additional Hooks](https://zh-hans.reactjs.org/docs/hooks-reference.html#additional-hooks) * [`useReducer`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usereducer) * [`useCallback`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usecallback) * [`useMemo`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usememo) * [`useRef`](https://zh-hans.reactjs.org/docs/hooks-reference.html#useref) * [`useImperativeHandle`](https://zh-hans.reactjs.org/docs/hooks-reference.html#useimperativehandle) * [`useLayoutEffect`](https://zh-hans.reactjs.org/docs/hooks-reference.html#uselayouteffect) * [`useDebugValue`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usedebugvalue) # `useState ()` 在初始渲染期间,返回的状态 (`state`) 与传入的第一个参数 (`initialState`) 值相同。 在后续的重新渲染中,`useState`返回的第一个值将始终是更新后最新的 state。 ``` function Counter({initialCount}) { // 返回一个数组 const [count, setCount] = useState(initialCount); // Declare multiple state variables! const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]); return ( <> Count: {count} <button onClick={() => setCount(initialCount)}>Reset</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> <button onClick={() => setCount(c=> c + 1)}>+</button> </> ); } ``` **第二个成员**是一个函数,用来更新状态,约定是`set`前缀加上状态的变量名(上例是`setButtonText`)。 > [2020要用immer来代替immutable优化你的React项目](https://blog.csdn.net/GetIdea/article/details/103770851) 小结: * React 会在重复渲染时保留 state。 * `useState`会返回一对值:**当前**状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。 * 更新函数 类似 class 组件的`this.setState`,但是它不会把新的 state 和旧的 state 进行合并。 * `setCount(c => c + 1)` // ✅ 在这不依赖于外部的 `count` 变量,每次`setCount`内部的回调取到的`count`是最新值(在回调中变量命名为`c`)。 # `useEffect ()` 默认情况下,第一次渲染之后和每次更新之后会运行 `useEffect()`。 effect 的全称应该是 Side Effect,中文名叫副作用,我们在前端开发中常见的副作用有: * dom 操作 * 浏览器事件绑定和取消绑定 * 发送 HTTP 请求 * 打印日志 * 访问系统状态 * 执行 IO 变更操作 示例: ``` // 添加了 数组项 personId 数据源依赖项时,根据该数据源是否变化来决定是否触发回调 useEffect(() => { setLoading(true); fetch(`https://swapi.co/api/people/${personId}/`) .then(response => response.json()) .then(data => { setPerson(data); setLoading(false); }); }, [personId]) ``` * 可以返回一个函数,来进行额外的[清理副作用](https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup)。(类似类组件的 `componentWillUnmount` 时触发) ``` useEffect(() => { const subscription = props.source.subscribe(); return () => { // 清理订阅 subscription.unsubscribe(); }; }); ``` * 可以将 `useEffect` Hook 看作 `componentDidMount`,`componentDidUpdate` 和`componentWillUnmount` 的组合。 整个组件的生命周期流程可以这么理解: > **组件挂载** --> 执行副作用 --> **组件更新** --> 执行清理函数 --> 执行副作用 --> ... --> **组件卸载** 小结: * React 会等待浏览器完成画面渲染之后才会延迟调用`useEffect`,因此会使得额外操作很方便 * effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次 * `useEffect`会在调用一个新的 effect 之前对前一个 effect 进行清理 * React 将按照 effect 声明的顺序依次调用组件中的每一个 effect * 如果想执行**只运行一次的 effect(仅在组件挂载/卸载(mount/unmount)时执行),可以传递一个空数组(`[]`)作为第二个参数**。这就告诉 React 你的 effect 不依赖于 `props` 或 `state` 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循输入数组的工作方式。 # `useContext ()` 1. 使用 React Context API,在组件外部建立一个 Context。 ``` import { createContext } from "react"; const AppContext =createContext(); ``` 组件封装代码如下: ``` <AppContext.Provider value={{ username: 'superawesome' }}> <div className="App"> <Navbar/> <Messages/> </div> </AppContext.Provider> ``` 上面代码中,`AppContext.Provider`提供了一个 Context 对象,这个对象可以被子组件共享。 2. 子组件需要 `useContext()` 钩子函数用来引入 Context 对象,从中获取 `username`属性。 ``` const Navbar = () => { const { username } = useContext(AppContext); return ( <div className="navbar"> <p>AwesomeSite</p> <p>{username}</p> </div> ); } ``` # `useReducer ()` Redux 的 Reducer 函数的形式是 `(state, action) => newState`。 `useReducer()` 钩子用来引入 Reducer 功能。 (😂:真不愧是 Redux 作者啊,还是用的 redux 的方式) > React 会确保`dispatch`函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从`useEffect`或`useCallback`的依赖列表中省略`dispatch`。 ``` import React, { useReducer } from "react"; ... // 定义 reducer const myReducer = (state, action) => { switch(action.type) { case('countUp'): return { ...state, count: state.count + 1 } default: return state } } function App() { // 参数为:reducer 和初始状态,返回的数组为:更新后的状态和 dispatch 用来分发状态。 const [state, dispatch] = useReducer(myReducer, { count: 0 }); return ( <div className="App"> <button onClick={() => dispatch({ type: 'countUp' })}> +1 </button> <p>Count: {state.count}</p> </div> ); } ... ``` # `useRef()` [`useRef()`](https://zh-hans.reactjs.org/docs/hooks-reference.html#useref)Hook 不仅可以用于 DOM refs。「ref」 对象是具有一个可变`current`属性且可以容纳任意值的通用容器,类似于一个 class 的实例属性 ``` // initialValue 可有可无,或者设置为 null const savedCallback = useRef(initialValue); function callback() { setCount(count + 1); } useEffect(() => { // 可以赋任意值到 current 属性中,进行保存 savedCallback.current = callback; }); ``` `useRef()` 返回**一个带有 `current` 可变属性(初始值为`initialValue`)的普通对象**在每一次渲染之间共享。 请记住,`useRef` 的内容更改时不会通知您。 更改 `current` 属性不会导致重新渲染。 # `useCallback` 与 `useMemo` 1. `useCallback` 返回一个[`memoized`](https://en.wikipedia.org/wiki/Memoization)回调函数。 2. `useMemo` 返回一个 任何值,参数:一个具有返回 value 的函数、依赖项数组)。 如果没有提供依赖项数组,`useMemo`在每次渲染时都会计算新的值。 > `useCallback(fn, deps)`相当于`useMemo(() => fn, deps)`。 优化示例: ``` ... function Foo({bar, baz}) { useEffect(() => { const options = {bar, baz} buzz(options) }, [bar, baz]) return <div>foobar</div> } function Blub() { const bar = useCallback(() => {}, []) // 传递了`[]`作为`useCallback`的依赖列表。这确保了 callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。 const baz = useMemo(() => [1, 2, 3], []) // 没有提供依赖项数组,`useMemo`在每次渲染时都会计算新的值 return <Foo bar={bar} baz={baz} /> } ... ``` 小结: * `useMemo`是在渲染期间执行的,所以`useMemo`中不要执行一些有副作用的操作。 * 没有提供依赖项数组,`useMemo`在每次渲染时都会计算新的值,依赖数组是空数组的话,`useMemo`中的函数就只会执行一次。 * 在实现 useMemo 时,你需要问问自己:“这真的是一个代价高昂的函数吗?” 代价高昂意味着它正在消耗大量资源(如内存)。如果在渲染时在函数中定义大量变量,则用 useMemo 进行记忆是非常有意义的。 > [什么时候使用 useMemo 和 useCallback-依赖列表](https://jancat.github.io/post/2019/translation-usememo-and-usecallback/#%E4%BE%9D%E8%B5%96%E5%88%97%E8%A1%A8) # Custom Hook 就是根据业务场景对以上四种 Hooks 进行组装,从而得到满足自己需求的钩子。 1. 一个 JavaScript 函数 2. 名称以`use` 开头 3. 可以调用其他钩子 > 自定义 hook 是一种自然遵循于 hook 设计的约定,而不是一个 React 特性 > 自定义 hook 没有特别的语法,就是利用:`state`的改变会引发函数组件重新执行这一特性! 比如,我们要将我们上面的代码功能封装成 Hooks, 代码如下 ``` import React, { useState, useEffect } from 'react' // 自定义的 Hook const usePerson = (name) => { const [loading, setLoading] = useState(true) const [person, setPerson] = useState({}) useEffect(() => { setLoading(true) setTimeout(()=> { setLoading(false) setPerson({name}) },2000) },[name]) return [loading,person] } const AsyncPage = ({name}) => { const [loading, person] = usePerson(name) return ( <> {loading?<p>Loading...</p>:<p>{person.name}</p>} </> ) } const PersonPage = () =>{ const [state, setState]=useState('') const changeName = (name) => { setState(name) } return ( <> <AsyncPage name={state}/> <button onClick={() => {changeName('名字1')}}>名字1</button> <button onClick={() => {changeName('名字2')}}>名字2</button> </> ) } export default PersonPage ``` 上面代码中,我们将之前的例子封装成了自己的 Hooks, 便于共享。其中,我们定义 `usePerson()` 为我们的自定义 Hooks,它接受一个字符串,返回一个数组,数组中包括两个数据的状态,之后我们在使用 `usePerson()` 时,会根据我们传入的参数不同而返回不同的状态(原理),然后很简便的应用于我们的页面中。 # 思考⚠️ 不要在循环,条件或嵌套函数中调用 Hook。 相反,请始终在您的 React 函数的顶层使用 Hook。 通过遵循此规则,可以确保每次渲染组件时都以相同的顺序调用 Hook。 这些是让 React 在多个`useState`和`useEffect`调用之间正确保留 Hook 的状态的原因。 > [https://reactjs.org/docs/hooks-rules.html#explanation](https://reactjs.org/docs/hooks-rules.html#explanation) > * **完全可选的。** 你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。 > * **100% 向后兼容的。** Hook 不包含任何破坏性改动。 > * **现在可用。** Hook 已发布于 v16.8.0。 > * **没有计划从 React 中移除 class。** > * **Hook 不会影响你对 React 概念的理解。** 恰恰相反,Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。 使用 React Hook 规则: https://zh-hans.reactjs.org/docs/hooks-rules.html 使用 ESlint 插件: https://www.npmjs.com/package/eslint-plugin-react-hooks # 常见问题 [setInterval() 与 Hooks](https://overreacted.io/zh-hans/making-setinterval-declarative-with-react-hooks/) [Writing Redux-like simple middleware for React Hooks](https://medium.com/front-end-weekly/writing-redux-like-simple-middleware-for-react-hooks-b163724a7058) # 相关库 [Awesome-React-Hooks](https://github.com/rehooks/awesome-react-hooks) ## Form [React Hook Form](https://react-hook-form.com/) Performant, flexible and extensible forms with easy-to-use validation ## React Hooks 库 [usehooks.com](https://usehooks.com/) [react-query](https://github.com/tannerlinsley/react-query) [SWR](https://github.com/zeit/swr) [react-use](https://github.com/streamich/react-use) [丰富的 Hooks](https://github.com/umijs/hooks) [React Custom Hooks 最佳实践](https://github.com/brickspert/blog/issues/31) # 参考 超性感的react hooks(六)自定义hooks的思维方式