序言
React Hooks自2018年10月的React conf上和React 16.7.0-alpha一起推出提案以来,一直维持着很高的社区热度。时隔将近2年,越来越多的团队和开发者开始拥抱Hooks和函数式组件,Hooks正在重塑整个React生态。本文将介绍如何基于Hooks生态构建一个全面的React应用。

路由管理
React Router
构建大型React应用离不开前端路由管理,有长达6年的历史,最成熟的解决方案React Router在其最新的5.x稳定版已经开始提供少量的Hooks API: useHistory, useLocation, useParams, useRouteMatch 可以取代以往的高阶组件withRouter,让函数组件能够轻松地获取和控制当前的路由状态。
而React Router和@reach/router合并后的v6 (2020/6/20, 6.0.0-beta.0) 更是激进地完全利用Hooks重构,带来了大量的Hooks API,甚至可以完全通过Hooks来操作整个前端路由:
- 定义路由:
useRoutes
import React from 'react';import { useRoutes } from 'react-router-dom';function App() {let element = useRoutes([{ path: '/', element: <Dashboard />, children: [{ path: 'messages', element: <DashboardMessages /> },{ path: 'tasks', element: <DashboardTasks /> }]},{ path: 'team', element: <AboutPage /> }]);return element;}
- 导航:编程式
useNavigate代替useHistory - 路由拦截:
useBlocker,usePrompt - 获取URL:相对路径
useResolvedPath, 参数useParams,useSearchParams - 更多…
除了更易用的API,Hooks重构还使得打包体积大大减少, min版的仅有8KB,Gizp之后更是只有3KB,而目前的v5.2.0则高达20.9KB和7.3KB,这也是React Router历史上体积最小的版本。

hookrouter
另一个小众的路由库hookrouter正如其名,也是一个基于Hooks的路由库,在2019/2/18发布了第一个版本1.0.0-beta。该库同样提供了诸如useRoutes,useQueryParams,navigate,useRedirect,拦截器等API,具备一个完备的路由应该有的功能。可惜的是该项目停止更新并停留在1.2.3已经整整一年了,而家大业大的React Router采用Hooks重写后,恢复更新的hookrouter还能否脱颖而出?
import {useRoutes} from 'hookrouter';const routes = {'/': () => <HomePage />,'/about': () => <AboutPage />,'/products': () => <ProductOverview />,'/products/:id': ({id}) => <ProductDetails id={id} />};const MyApp = () => {const routeResult = useRoutes(routes);return routeResult || <NotFoundPage />;}
数据流管理
React Redux
大名鼎鼎的状态管理库Redux的官方React库,这也是大多数开发者绕不开的一个状态管理库。react-redux在去年的7.1.0版本也加入了对Hooks的支持,足以代替高阶组件connect来订阅redux store和发送(dispatch) actions到store。只需要用单个<Provider>组件包裹应用,整个应用的组件都能通过这3个主要Hooks进行状态管理。
mapStateToProps==>useSelector(selector: Function, equalityFn?: Function)
import React from 'react'import { useSelector } from 'react-redux'const Counter = () => {const counter = useSelector(store => store.counter)return <div>{counter}</div>}
selector并不限于普通箭头函数的形式,官方的例子中还有闭包,柯里化和记忆化的实现。- 可选参数
equalityFn用于判断是否相等并强制重渲染,默认是===的引用比较,还可以使用官方的浅比较shallowEqual和Lodash的深比较_isEqual等方法。 mapDispatchToProps==>useDispatch- 可以使用
useMemo()或useCallback()记忆化后再传递action给子组件避免引用变化带来的不必要重渲染。
- 可以使用
useStore简单直接地获取整个store。
MobX React
另一个著名的Mutable状态管理库MobX在18年底推出了mobx-react-lite,一个仅支持函数组件和Hooks的轻量级版本,而去年6月发布的mobx-react@6中也在lite的基础上正式推出了Hooks API 。但官方依旧推荐在新项目和没有类组件的项目中直接使用更轻量和高性能的mobx-react-lite。
MobX核心概念
- 创建可观察状态:
useLocalStore - 观察状态改变:
useObserver
简单例子:
import React from 'react'import { useLocalStore, useObserver } from 'mobx-react' // 6.xexport const SmartTodo = () => {const todo = useLocalStore(() => ({title: 'Click to toggle',done: false,toggle() {todo.done = !todo.done},get emoji() {return todo.done ? '😜' : '🏃'},}))return useObserver(() => (<h3 onClick={todo.toggle}>{todo.title} {todo.emoji}</h3>))}
此外还可以通过React Context建立全局的MobX Store:
import React from 'react'import { useLocalStore } from 'mobx-react-lite'export function createStore() {return {friends: [],makeFriend(name, isFavorite = false, isSingle = false) {this.friends.push({ name, isFavorite, isSingle })},get singleFriends() {return this.friends.filter(friend => friend.isSingle)},}}const storeContext = React.createContext(null)// 将StoreProvider放在应用最外层export const StoreProvider = ({ children }) => {const store = useLocalStore(createStore)return <storeContext.Provider value={store}>{children}</storeContext.Provider>}// 封装useContextexport const useStore = () => React.useContext(storeContext)
使用Store:
import React from 'react'import { useStore } from './store'import { useObserver } from "mobx-react-lite";export const FriendsMaker = () => {const store = useStore();// action修改store状态const onSubmit = ({ name, favorite, single }) =>store.makeFriend(name, favorite, single);// useObserver 会监听store变更并更新组件return useObserver(() => (<div><p>Total friends: {store.friends.length}</p><p>Single friends: {store.singleFriends}</p><form onSubmit={onSubmit}><input type="text" id="name" /><input type="checkbox" id="favorite" /><input type="checkbox" id="single" /></form></div>));};
Unstated-Next
从重量级的状态管理库回过头来仔细思考,我们真的需要Redux和MobX吗?实际上React内置的Context 和Hooks 足以胜任大多跨组件数据共享场景。用起来也相当的简单和直观直观,具体数据管理完全由用户在自定义Hook中定义:
import React, { useState, useContext } from "react"function useCounter(initialState = 0) {let [count, setCount] = useState(initialState)let decrement = () => setCount(count - 1)return { count, increment }}const Counter = React.createContext(null)function CounterExample() {const counter = useContext(Counter)return <button onClick={counter.increment}>{counter.count}</button>}function App(){const counter = useCounter()return (<Counter.Provider value={counter}><CounterExample /></Counter.Provider>)}
而不到40行代码的unstated-next就是封装自定义Hooks来实现完备的状态管理。
export function createContainer(useHook) {let Context = React.createContext(null);// 自定义Hooks返回值直接传递给Providerfunction Provider(props) {let value = useHook(props.initialState);return <Context.Provider value={value}>{props.children}</Context.Provider>;}// 封装useContext获取storefunction useContainer() {let value = React.useContext(Context);if (value === null) {throw new Error("Component must be wrapped with <Container.Provider>");}return value;}return { Provider, useContainer };}
而同样的例子使用unstated-next是这样的:
import React, { useState } from "react"import { createContainer } from "unstated-next"// 1. 自定义Hookfunction useCounter(initialState = 0) {let [count, setCount] = useState(initialState)let decrement = () => setCount(count - 1)return { count, increment }}// 2. 封装为数据对象const Counter = createContainer(useCounter)// 3. 使用数据function CounterExample() {const counter = Counter.useContainer()return <button onClick={counter.increment}>{counter.count}</button>}// 4. 包裹子组件function App(){return (<Counter.Provider><CounterExample /></Counter.Provider>)}
其实代码的逻辑并没有改变,但unstated-next通过createContainer将Provider和Context约束到一个Container中,进一步规范了React Hooks作为状态管理的能力。
Recoil
出自Facebook的状态管理库Recoil自今年5月推出以来就受到不少关注,同样是基于Context和Hooks,recoil的代码复杂度远远高过于unstated-next,并不是简单的逻辑封装。recoil构建了一个数据流图:atoms(即共享状态的最小单元)通过selectors流入到React组件中。
- 像大多数状态管理库一样,recoil同样需要在应用最外层包裹
RecoilRoot
import React from 'react'import { RecoilRoot } from 'recoil'function App() {return (<RecoilRoot><CounterExample /></RecoilRoot>)}
- 然后分散地定义atom - recoil中的原子状态,每一个使用到atom的组件都会隐式地订阅atom的更新并在atom更新后重新渲染。
const counterState = atom({key: 'counterState', // 全局唯一key来表示atomdefault: 0 // 默认值也是分散定义})
- 或者通过selector获取atom的派生值,其中获取派生值的
get支持同步或者异步,既可以直接返回值也可以返回一个函数。selector使得数据流图的atoms状态能够互相依赖,避免定义多余的atom。
const doubleCounterState = selector({key: "doubleCounterState",get: ({ get }) => get(counterState) * 2, // get可以获取其他atom/selector的值set: ({ set }, newValue) => set(counterState, newValue * 2) // 提供可选的set让selector返回一个可写的状态对象});
- recoil提供了多达6个消费atoms/selectors的API:
useRecoilState: 类似于Hooks的useState,可以读写atoms
const [counter, setCounter] = useRecoilState(counterState)
useRecoilValue: 仅获取atoms/selector的值useSetRecoilState: 仅返回写atoms的setter函数,该方式不会订阅atoms的更新因此不会导致重渲染useResetRecoilState:返回一个重设atoms为默认值的函数,同样不订阅atoms的更新useRecoilValueLoadable: 用于仅读取异步的selector
function UserInfo({userID}) {const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID))switch (userNameLoadable.state) {case 'hasValue':return <div>{userNameLoadable.contents}</div>;case 'loading':return <div>Loading...</div>;case 'hasError':throw userNameLoadable.contents}}
useRecoilStateLoadable: 用于读写异步selector
此外还有SnapShot(获取全局Store)相关的API和一些工具函数,可以看出recoil的API还是偏多的。本质上还是因为Immutable模式中需要对数据的读写区分开,全局状态管理必然存在读写分离的情况,为了仅写的组件性能最优化就只能额外的设计仅读/写的API。
use-immer
React性能优化中经常会用到shouldComponentUpdate()、PureComponent和React.memo (这两者都是浅比较) 来避免不必要的渲染,其主要目的是让组件只有数据发生变更时才重新渲染。但对于对象和数组而言,浅比较不能判断数据是否发生改变,而每次都在shouldComponentUpdate()进行深比较会非常影响性能。
而Immutable数据结构就是创建后不可更改的数据,每次变更都会返回一个新的Immutable数据,但保持旧数据的引用不变来避免深拷贝,即只会影响被修改的节点和其父节点。
使用Immutable数据的方式有很多,其中一个轻量级实现Immer就是通过Proxy生成当前对象的临时draft对象,对draft的直接修改会反映到produce函数的返回对象中。
在useState Hook中的数据必须为Immutable,要求每次setState返回的是不同的对象,这对修改具有复杂结构的对象并不友好,这时就可以用use-immer (将Immer封装成一个Hook) 来代替useState:
import React from "react";import { useImmer } from "use-immer";export default function App() {const [state, updateState] = useImmer({people: [{name: "john",age: 19},{name: "mike",age: 10},{name: "jack",age: 28}]});const updateName = (index, name) => {updateState(draft => {draft.people[index].name = name;});};const becomeOlder = index => {updateState(draft => {draft.people[index].age++;});};const renderPerson = (index, name, age) => (<div><h2>{name}:{age}</h2><input onChange={e => updateName(index, e.target.value)} value={name} /><button onClick={() => becomeOlder(index)}>Older</button></div>);return (<div className="App">{state.people.map((person, index) =>renderPerson(index, person.name, person.age))}</div>);}
除了useImmer,还提供了useReducer的封装useImmerReducer:
import React from "react";import { useImmerReducer } from "use-immer";const initialState = { count: 0 };function reducer(draft, action) {switch (action.type) {case "reset":return initialState;case "increment":return void draft.count++;case "decrement":return void draft.count--;}}function Counter() {const [state, dispatch] = useImmerReducer(reducer, initialState);return (<>Count: {state.count}<button onClick={() => dispatch({ type: "reset" })}>Reset</button><button onClick={() => dispatch({ type: "increment" })}>+</button><button onClick={() => dispatch({ type: "decrement" })}>-</button></>);}
dva
dva是基于redux和redux-saga的数据流管理方案。虽然目前最新正式版仍旧是2年前发布的2.4.1并不支持Hooks,但已经处于beta状态1年多的2.6.0版本更新了内置的react-redux、redux-router-dom (目前2.6.0-beta.20分别是7.1.0和5.1.2),所以实际上也能使用这两个库的Hooks API。umijs的dva插件默认使用的就是^2.6.0-beta.20。
issues#2324和issue#2333中官方提到会在下半年推出dva 3.0,并且带来以下新特性:
- 全面支持typescript
- 升级到redux saga 1.x
- 默认提供Immer
- 弱化reducer和subscription
UI组件库
大多数大型应用会选择成熟的UI组件库来加速开发,在Hooks推出后,不少UI组件库也开始用Hooks重写并提供Hooks API。
Ant design
作为社区上最流行的React UI库 (61.8K stars, 447K npm周下载),蚂蚁的Ant Design在今年初发布的v4中部分组件开始用Hooks进行重构并提供Hooks API,官网的部分文档也改用Hooks重写。截止目前(2020/7),
Hooks 重构
- Input.Password
- List 列表
- Alert 警告提示
- BackTop 回到顶部
- Tooltip 文字提示
- Timeline 时间轴
Hooks API
- Grid 栅格提供了
useBreakpoint - Form 表单提供
useForm创建form实例对表单数据进行交互 - Modal 对话框提供
useModal获取modal实例和要插入的contextHolder节点 - Notification 通知提供
useNotification与useModal作用类似
Sunflower
蚂蚁的Sunflower是基于antd的流程组件,通过React Hooks来描述antd组件间的流程关系,sunflower会返回这些组件的props,只要将props传递给组件就能完成组件的联系。目前提供了4类流程组件:
useFormTable:使用Form搜索并用Table展示列表useCascadeSelect:多个选择器的级联选择useModalForm:弹窗表单useStepsForm:分步表单
使用例子:
import React from 'react';import { useFormTable } from 'sunflower-antd';import { Input, Button, Table, Form } from 'antd';export default props => {const { formProps, tableProps } = useFormTable({async search(values) {const res = await request(values);return res;},});return (<div><Form {...formProps}><Form.Item label="Username" name="username"><Input placeholder="Username" /></Form.Item><Form.Item><Button type="primary" htmlType="submit">Search</Button></Form.Item></Form><Tablecolumns={[{title: 'Username',dataIndex: 'username',key: 'username',},]}{...tableProps}/></div>);};
React Hook Form
React Hook Form是一个基于Hooks的高性能,小体积(仅有5KB)的表单校验库。和Formilk不同,React Hook Form并不需要引入额外的组件来包裹表单,而是将非受控的输入组件注册(ref)到Hook中,获取输入的值用于表单校验和提交。
import React from 'react'import useForm from 'react-hook-form'function App() {const { register, handleSubmit, errors } = useForm() // 初始化Hookconst onSubmit = (data) => { console.log(data) } // 输入有效时提交return (<form onSubmit={handleSubmit(onSubmit)}><input name="firstname" ref={register} /> // 注册输入<input name="lastname" ref={register({ required: true })} /> // 注册为必须输入{errors.lastname && 'Last name is required.'} //姓氏无效时显示错误消息<input name="age" ref={register({ pattern: /\d+/ })} /> // 用模式注册为输入{errors.age && 'Please enter number for age.'} // 年龄无效时显示错误消息<input type="submit" /></form>)}
数据请求
React本身并没有提供从组件请求/更新数据的方式,开发者通常需要根据业务封装自己的一套数据请求方案和处理异步的状态逻辑。React Hooks的出现使得这些异步数据请求逻辑能够复用并封装为一个工具库:
swr
swr是vercel推出的React Hook数据请求库,通过一个简单的useSWR API默认提供了自定义请求、依赖请求、自动刷新、间隔轮询、局部突变、获得焦点/网络恢复时重新请求、快速页面导航(利用缓存)、分页按需更新、滚动位置恢复等众多异步请求特性。
SWR (stale-while-revalidate) 是一种策略,用于首先从缓存返回数据,然后发送提取请求(重新验证) ,最后提供最新数据。
const { data, error, isValidating, mutate } = useSWR(key, fetcher, options)接收3个参数:
- key:唯一的请求字符串/函数/null
- fetcher:可以说
fetch或者axios等请求的封装,接受key作为第一个参数 - options:可选的配置项
import useSWR from 'swr'function Profile() {const { data, error } = useSWR('/api/user', fetcher)if (error) return <div>failed to load</div>if (!data) return <div>loading...</div>return <div>hello {data.name}!</div>}
基于swr的特性,可以将api请求封装为自定义Hook,swr会在不同组件中自动缓存和共享请求的结果,实际上只会发送一个请求:
function useUser() {const { data, error } = useSWR('/api/user', fetcher)return {user: data,isLoading: !error && !data,isError: error}}function Example1() {const { user, isLoading } = useUser()if (isLoading) return <Loading />return <div>Example1: {user}</div>}function Example2() {const { user, isLoading } = useUser() // 共用请求缓存if (isLoading) return <Loading />return <div>Example2: {user}</div>}
因此swr也可以用作全局的状态管理,官方例子local-state-sharing中就使用mutate的方式实现了组件间的状态同步。
react-query
react-query也是一个提供Hooks API的异步数据请求库,功能上与swr类似。但额外提供了useMutation用于创建/更新/删除数据或执行服务器副作用。
import { useQuery, useMutation, queryCache } from 'react-query'import { getTodos, postTodo } from '../my-api'function Todos() {// Queriesconst todosQuery = useQuery('todos', getTodos)// Mutationsconst [addTodo] = useMutation(postTodo, {onSuccess: () => {// Query InvalidationsqueryCache.invalidateQueries('todos')},})return (<div><ul>{todosQuery.data.map(todo => (<li key={todo.id}>{todo.title}</li>))}</ul><buttononClick={() =>addTodo({id: Date.now()title: 'Do Laundry',})}>Add Todo</button></div>)}
常用逻辑库
因为React Hooks提出的动机之一就是能够不修改组件逻辑的情况下复用状态逻辑,通过自定义Hook就可以将组件逻辑提取到能够被其他组件复用的函数中,这种能力使得社区上涌现出大量基于Hooks的工具库。
react-use
react-use是社区上最流行的React Hooks库(14.1K stars, 193K npm周下载)。更新频率也是相当的高,自Hooks提出的18年10月至今迭代了243个版本。react-use致力于提供各类常用细粒度逻辑的封装,目前(v15.3)已有113
个Hooks,包含Sensor 传感器、State 状态、副作用 Side Effect、UI、生命周期 Life Cycle, 动画 Animation等类别。以下是一些个人认为比较常用的Hooks。
useScrolling: 获取页面是否在滚动useSize:获取元素的尺寸useWindowSize: 获取浏览器窗口尺寸usePrevious:获取上一个State的值useList/Map/Set:分别是封装了array,Map和Set的Hook,提供操作数据的方法useQueue:封装了FIFO的队列useToggle:切换布尔值useAsync/useAsyncFn:封装了异步操作useCookie/useLocalStorage/useSessionStorage:封装了Cookie、LocalStorage和SessionStorage相关的操作useCopyToClipboard:封装了读写剪贴板的操作useDeounce:防抖useThrottle/useThrottleFn:节流useCss:动态CSSuseFullscreen:切换元素全屏useEffectOnce:只执行一次EffectuseInterval/useTimeout:封装定时器
ahooks
ahooks是阿里巴巴面向中台应用场景的开源React Hooks工具库,提供了异步请求、表格封装、UI、副作用、生命周期、状态、DOM等7大类共48个Hooks。虽然数量上不如react-use多,但也提供了不少有特色的Hooks:
useRequest:受到swr启发的异步请求管理Hook,也是umi 3.0中内置的请求方案,包含了轮询、并行请求、防抖、节流、缓存、预加载、分页等常用的请求相关操作useAntdTable/useFusionTable:和sunflower的流程组件类似,封装了Form和Table的常用联动逻辑useSelections:checkbox多选框联动逻辑的封装useVirtualList:虚拟化列表useHistoryTravel:状态变化历史管理useTextSelection:获取当前选中的文本内容和位置useEventEmitter:多组件共享事件通知
react-adaptive-hooks
Google Chrome Labs团队的react-adaptive-hooks封装了获取用户设备状态的Hooks,包含以下5个设备状态相关的Hooks,主要用于自适应地实现资源加载、获取数据、代码分割和功能切换等功能。
useNetworkStatus:获取网络状态useSaveData:获取浏览器数据节省首选项useHardwareConcurrency:获取设备逻辑CPU核心数量useMemoryStatus:获取设备内存useMediaCapabilitiesDecodingInfo:获取设备媒体能力
更具体的介绍可以参看这篇文章React Adaptive Hooks and APIs for Adaptability
更多Hooks集合
不同的库中封装的Hooks实际上是大同小异的(无论是功能上还是实现上),一般来说只需要引入一个React Hooks工具库就能满足大部分需求。
CSS in JS
React应用的组件化样式处理方式有很多,CSS in JS就是其中一个选择。除了高阶组件用法外,两个流行CSS in JS库都提供了基于React Context的useTheme API使子组件订阅全局的主题,实现方式其实也是大同小异的。
Emotion
import { jsx } from '@emotion/core'import styled from '@emotion/styled'import { ThemeProvider, useTheme } from 'emotion-theming'const theme = {colors: {primary: 'hotpink'}}function SomeText (props) {const theme = useTheme()return (<divcss={{ color: theme.colors.primary }}{...props}/>)}render(<ThemeProvider theme={theme}><SomeText>some text</SomeText></ThemeProvider>)
此外未来的Emotion 11也计划使用Hooks进行重写部分代码来减少包体积 (issue#1606)。
国际化
国际化是大型前端应用绕不开的一个话题,不仅要考虑多语言的翻译,还涉及到日期、货币等的格式化。React常用到的2个国际化库react-intl和react-i18next都已经提供了Hook来代替HoC向组件注入国际化。
react-intl
Format.js推出的react-intl提供了基础的useIntl来获取intl对象的formatDate和formatMessage等api,也可以根据需要将useIntl封装到自定义Hook中来更方便地使用各个format API。
import React from 'react';import { useIntl } from 'react-intl';export default const Example = ({date}) => {const intl = useIntl();return (<span title={intl.formatDate(date)}>{intl.formatMessage({id: 'app.test',defaultMessage: 'Test'})}</span>);};
react-i18next
另一个国际化库react-i18next则提供了useTranslation Hook,返回翻译函数和i18n实例:
import React from 'react';import { useTranslation } from 'react-i18next';export function Example() {const [t, i18n] = useTranslation();return <p>{t('my translated text')}</p>}
基本上函数组件都可以用useTranslation 这个Hook代替高阶组件withTranslation 。
动画
react-spring
react-spring是基于弹簧物理效果的React动画库,不同于基于时间的传统动画,弹簧没有固定的曲线和持续时间,因此react-spring的动画更加简单易懂。在Hooks推出后react-spring也推出了Hooks API并取代原有的Render Props API,目前提供了5个Hook API:
useSpring:单个弹簧,从A到BuseSprings:多个弹簧,每个弹簧都是A到BuseTrail:多个弹簧,每个弹簧跟在前一个弹簧之后useTransition:用于挂载或移除transitionuseChain:使多个动画形成队列或连接多个动画
官方示例:
import React from 'react'import ReactDOM from 'react-dom'import { useSpring, animated } from 'react-spring'import './styles.css'const calc = (x, y) => [-(y - window.innerHeight / 2) / 20, (x - window.innerWidth / 2) / 20, 1.1]const trans = (x, y, s) => `perspective(600px) rotateX(${x}deg) rotateY(${y}deg) scale(${s})`function Card() {const [props, set] = useSpring(() => ({xys: [0, 0, 1],config: { mass: 5, tension: 350, friction: 40 }}))return (<animated.divclass="card"onMouseMove={({ clientX: x, clientY: y }) => set({ xys: calc(x, y) })}onMouseLeave={() => set({ xys: [0, 0, 1] })}style={{ transform: props.xys.interpolate(trans) }}/>)}ReactDOM.render(<Card />, document.getElementById('root'))

3D卡片动画效果
总结
在常用第三方库陆续支持后,2020年的React应用已经完全可以摆脱高阶组件、render props和臃肿的类组件,在函数中用更少的代码更高效地编写组件,同时React Hooks也催生了社区中大量有趣的新想法诞生(例如reocil, react hook form, swr和ahooks),可以说生态的缺失已经不是拒绝Hooks的理由了。
