React hooks 发布于 React V16.8.0,想了解 react-hooks 基本概念的同窗能够参考 官方文档,想深刻了解 useEffect 的同窗能够看一下 Dan Abramov 写的 A complete guide to useEffect,对于 react-hooks 的 rules 有疑惑的同窗能够参考 React hooks:not magic, just arrays 这一篇文章。
这篇文章的目的在于记录我在学习和应用 react-hooks 过程当中的一些感悟,也但愿能对你学习和应用 react-hooks 提供帮助。html
以前 React 对于状态管理没有提供很好的支持,因此咱们会依赖 redux 和 mobx 这些第三方的状态管理库。redux 很好地实践了 immutable 和 pure function;mobx 则是 mutable 和 reactive 的表明。如今使用 React 内置 useReducer 和 useContext 这两个 hook 可让咱们实现 redux 风格的状态管理;结合 useMemo 和 useEffect 能够模拟 mobx 的 computed 和 reaction。(hooks 没有使用 Proxy,因此跟 mobx 的代理响应是不同的)
下面咱们使用 hooks 实现一个原生的状态管理库。前端
跟 redux 同样,咱们使用 React Context 实现 global store:node
// store.ts import { useContext, createContext, useReducer } from 'react' // User 和 Env 是 reducer 文件 import { default as User } from './User' import { default as Env } from './Env' // 建立一个全局 context // 这里使用了 ReturnType 做为 Context 的范型声明 export const GlobalContext = createContext<ReturnType<typeof useGlobalStore>>( null ) // custom hook:封装全部全局数据,用于 GlobalContext.Provider 的 value 属性赋值 export function useGlobalStore() { const currentUser = useReducer(User.reducer, User.init()) const env = useReducer(Env.reducer, Env.init()) return { currentUser, env, } } // custom hook:实现了 GlobalContext.Consumer 的功能 export function useGlobal() { return useContext(GlobalContext) }
上面的代码定义了 store 模块做为项目的全局状态库,导出了三个实体:react
下面是使用 store 模块封装的 Provider 组件:git
// GlobalProvider.tsx import React from 'react' import { GlobalContext, useGlobalStore } from './store' export function GlobalProvider({ children }) { const store = useGlobalStore() return ( <GlobalContext.Provider value={store}>{children}</GlobalContext.Provider> ) }
将 Provider 模块引用到项目根组件:typescript
// App.tsx import React from 'react' import { GlobalProvider } from './components' import { Home } from './Home' export function App() { return <GlobalProvider><Home /></GlobalProvider> }
如今能够在 Home 组件里面消费 store:npm
// Home.tsx import React from 'react' import { useGlobal } from './store' export default function Home() { const { currentUser } = useGlobal() const [user, dispatch] = currentUser // 使用 dispatch 修改用户姓名 function changeName(event) { dispatch({ type: 'update_name', payload: event.target.value }) } return <div> <h2>{user.name}</h2> <input onChange={changeName} /> </div> }
下面是定义 reducer 的 User.ts 代码:redux
// User.ts function init() { return { name: '' } } const reducer = (state, { type, payload }) => { switch(type) { case 'UPDATE_NAME': { return { ...state, name: payload } } case 'UPDATE': { return { ...state, ...payload } } case 'INIT': { return init() } default: { return state } } } export default { init, reducer }
上面的 store 已经可以正常运做了,可是还有优化空间,咱们再次聚焦到 Home.tsx 上,有些同窗应该已经发现了问题:数据结构
// Home.tsx import React from 'react' import { useGlobal } from './store' export default function Home() { const { currentUser } = useGlobal() const [user, dispatch] = currentUser function changeName(event) { dispatch({ // 这里的 update_name 与 User.ts 中定义的 UPDATE_NAME 不一致 // 这是个 bug,可是只有在运行时会报错 type: 'update_name', payload: event.target.value }) } return <div> <h2>{user.name}</h2> <input onChange={changeName} /> </div> }
咱们须要加一些类型提示🤔react-router
咱们先肯定下指望,咱们但愿 reducer typing 能带给咱们的提示有:
上面的粗体部分是咱们指望 typing 提供的 4 个功能,让咱们尝试解决一下:
// User.ts // IUser 做为用户数据结构类型 type IUser = { name: string } function init() { return { name: '', } } // 使用 Union Types 枚举全部的 action type ActionType = | { type: 'INIT' payload: void } | { type: 'UPDATE_NAME' payload: string } | { type: 'UPDATE' payload: IUser } const reducer = (state, { type, payload }: ActionType) => { switch (type) { case 'UPDATE_NAME': { return { ...state, name: payload } } case 'UPDATE': { return { ...state, ...payload } } case 'INIT': { return init() } default: { return state } } } export default { init, reducer, }
看起来好像很不错,可是。。。
typescript 把 type 和 payload 两个字段分别作了 union,致使对象展开符报错了。
咱们能够把 payload 定义成 any 解决这个问题,可是就会失去上面指望的对于 paylod 的类型提示。
一种更健壮的方案是使用 Type Guard,代码看起来会像下面这样:
// User.ts type IUser = { name: string } function init() { return { name: '', } } type InitAction = { type: 'INIT' payload: void } type UpdateNameAction = { type: 'UPDATE_NAME' payload: string } type UpdateAction = { type: 'UPDATE' payload: IUser } type ActionType = InitAction | UpdateNameAction | UpdateAction function isInitAction(action): action is InitAction { return action.type === 'INIT' } function isUpdateNameAction(action): action is UpdateNameAction { return action.type === 'UPDATE_NAME' } function isUpdateAction(action): action is UpdateAction { return action.type === 'UPDATE' } const reducer = (state, action: ActionType) => { if (isUpdateNameAction(action)) { return { ...state, name: action.payload, } } if (isUpdateAction(action)) { return { ...state, ...action.payload, } } if (isInitAction(action)) { return init() } return state } export default { init, reducer, }
咱们获得了咱们想要的:
可是每写一个 action 都要加一个 guard,太浪费宝贵的时间了。让咱们用 deox 这个库优化一下咱们的代码:
// User.ts import { createReducer, createActionCreator } from 'deox' type IUser = { name: string } function init() { return { name: '', } } const reducer = createReducer(init(), action => [ action(createActionCreator('INIT'), () => init()), action( createActionCreator('UPDATE', resolve => (payload: IUser) => resolve(payload) ), (state, { payload }) => ({ ...state, ...payload, }) ), action( createActionCreator('UPDATE_NAME', resolve => (payload: string) => resolve(payload) ), (state, { payload }) => ({ ...state, name: payload, }) ), ]) export default { init, reducer, }
让咱们再作一些小小的优化,把繁重的 createActionCreator 函数简化一下:
// User.ts import { createReducer, createActionCreator } from 'deox' type IUser = { name: string } function init() { return { name: '', } } // 简化之后的 createAction 须要调用两次 // 第一次调用使用类型推断出 action type // 第二次调用使用范型声明 payload type function createAction<K extends string>(name: K) { return function _createAction<T = void, M = void>() { return createActionCreator(name, resolve => (payload: T, meta?: M) => resolve(payload, meta) ) } } const reducer = createReducer(init(), action => [ action(createAction('INIT')(), () => init()), action(createAction('UPDATE')<IUser>(), (state, { payload }) => ({ ...state, ...payload, })), action(createAction('UPDATE_NAME')<string>(), (state, { payload }) => ({ ...state, name: payload, })), ]) export default { init, reducer, }
大功告成,能够开始愉快地写代码了😊
开发前端页面的时候会遇到不少页面自适应的需求,这个时候子组件就须要根据父组件的宽高来调整本身的尺寸,咱们能够开发一个获取父容器 boundingClientRect 的 hook,获取 boundingClientRect 在 React 官网有介绍:
function useClientRect() { const [rect, setRect] = useState(null); const ref = useCallback(node => { if (node !== null) { setRect(node.getBoundingClientRect()); } }, []); return [rect, ref]; }
咱们扩展一下让它支持监听页面自适应:
// useLayoutRect.ts import { useState, useEffect, useRef } from 'react' export function useLayoutRect(): [ ClientRect, React.MutableRefObject<any> ] { const [rect, setRect] = useState({ width: 0, height: 0, left: 0, top: 0, bottom: 0, right: 0, }) const ref = useRef(null) const getClientRect = () => { if (ref.current) { setRect(ref.current.getBoundingClientRect()) } } // 监听 window resize 更新 rect useEffect(() => { getClientRect() window.addEventListener('resize', getClientRect) return () => { window.removeEventListener('resize', getClientRect) } }, []) return [rect, ref] }
你能够这么使用:
export function ResizeDiv() { const [rect, ref] = useLayoutRect() return <div ref={ref}>The width of div is {rect.width}px</div> }
若是你的需求只是监听 window resize 的话,这个 hook 写到这里就能够了,可是若是你须要监听其余会引发界面尺寸变动的事件(好比菜单的伸缩)时要怎么办?让咱们改造一下 useLayoutRect 让它更加灵活:
// useLayoutRect.ts import { useState, useEffect, useRef } from 'react' export function useLayoutRect(): [ ClientRect, React.MutableRefObject<any>, () => void ] { ... const getClientRect = () => { if (ref.current) { setRect(ref.current.getBoundingClientRect()) } } ... // 额外导出 getClientRect 方法 return [rect, ref, getClientRect] }
上面的代码咱们多导出了 getClientRect 方法,而后咱们能够组合成新的 useResize hook:
// useResize.ts import { useLayoutRect } from '@/utils' import { useGlobal } from './store' import { useEffect } from 'react' export function useResize(): [ClientRect, React.MutableRefObject<any>] { // menuExpanded 存在 global store 中,用于获取菜单的伸缩状态 const { env } = useGlobal() const [{ menuExpanded }] = env const [rect, ref, resize] = useLayoutRect() // 由于菜单伸缩有 300ms 的动画,咱们须要加个延时 useEffect(() => { setTimeout(resize, 300) }, [menuExpanded]) return [rect, ref] }
你能够在 useResize 中添加其余的业务逻辑,hooks 具备很好的组合性和灵活性👍
Charles Stover 在 git 上发布了一个很好用的 react-router hook,能够在 functional component 中实现 withRouter 的功能。实现的原理能够参考他的这篇博客 How to convert withRouter to a react hook。使用方法以下:
import useReactRouter from 'use-react-router'; const MyPath = () => { const { history, location, match } = useReactRouter(); return ( <div> My location is {location.pathname}! </div> ) }
学习 react hooks 的过程当中愈发以为 hooks 的强大,也更加能理解为何说 react hooks 是 future。相信随着时间的发展,社区可以创造出愈来愈的 custom hook,也会涌现出愈来愈多像 hooks 这样开创性的设计。