react-hooks toolkit
2020/1/7 5:25:22
本文主要是介绍react-hooks toolkit,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
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 提供帮助。
State Management
之前 React 对于状态管理没有提供很好的支持,所以我们会依赖 redux 和 mobx 这些第三方的状态管理库。redux 很好地实践了 immutable 和 pure function;mobx 则是 mutable 和 reactive 的代表。现在使用 React 内置 useReducer 和 useContext 这两个 hook 可以让我们实现 redux 风格的状态管理;结合 useMemo 和 useEffect 可以模拟 mobx 的 computed 和 reaction。(hooks 没有使用 Proxy,所以跟 mobx 的代理响应是不一样的)
下面我们使用 hooks 实现一个原生的状态管理库。
Global Store
跟 redux 一样,我们使用 React Context 实现 global store:
// 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 模块作为项目的全局状态库,导出了三个实体:
- GlobalContext:全局 context,用于链接 store 和相关组件;
- useGlobalStore:自定义 hook,将多个数据模块封装在一起,用于 GlobalContext.Provider 的 value 属性;
- useGlobal:自定义 hook,用于子组件引用全局 store;
Provider
下面是使用 store 模块封装的 Provider 组件:
// 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 模块引用到项目根组件:
// App.tsx import React from 'react' import { GlobalProvider } from './components' import { Home } from './Home' export function App() { return <GlobalProvider><Home /></GlobalProvider> }
Consumer
现在可以在 Home 组件里面消费 store:
// 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
下面是定义 reducer 的 User.ts 代码:
// 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 }
Reducer Typing
上面的 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> }
我们需要加一些类型提示🤔
Have a try
我们先确定下期望,我们希望 reducer typing 能带给我们的提示有:
- 在 Consumer 中调用 dispatch,输入 type 以后会显示该 reducer 相关的所有 action type 名称,输入不存在的 type 会有 错误提示;
- 在第一步输入正确的 type 以后,再输入 payload,会有对应 type 的 payload 的类型提示,输入不一致的 payload 会有 错误提示;
上面的粗体部分是我们期望 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, }
我们得到了我们想要的:
-
输入 type 以后会显示该 reducer 相关的所有 action type 名称
- 输入不存在的 type 会有 错误提示
-
payload 的类型提示
- 输入不一致的 payload 会有 错误提示
但是每写一个 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, }
大功告成,可以开始愉快地写代码了😊
useResize
开发前端页面的时候会遇到很多页面自适应的需求,这个时候子组件就需要根据父组件的宽高来调整自己的尺寸,我们可以开发一个获取父容器 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 具有很好的组合性和灵活性👍
useReactRouter
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 这样开创性的设计。
这篇关于react-hooks toolkit的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-05-09一定要避坑:关于微信H5分享,温馨提示你不要再踩坑了!!!
- 2024-05-09本地项目放到公网访问!炒鸡煎蛋!
- 2024-04-07金融企业区域集中库的设计构想和测试验证
- 2024-03-11前端CSS的工程化——掌握Sass这四大特性就够了
- 2024-02-21h5关联css样式(html怎么和css关联)-icode9专业技术文章分享
- 2024-02-07Firefox禁止远程字体加速网页加载及图标字体补充安装
- 2024-02-07一个菜鸡前端的3年总结-「2023」
- 2024-01-18最火前端Web组态软件(可视化)
- 2024-01-12程序员提效 x10 的必备开源“神器”
- 2024-01-11前端可以监控静态资源的时间吗-icode9专业技术文章分享