保护大型项目 OR UI 组件模块时,肯定会遇到全局数据传递问题。

保护我的项目时,像全局用户信息、全局我的项目配置、全局性能配置等等,都是跨模块复用的全局数据。

保护 UI 组件时,调用组件的入口只有一个,但组件外部会持续拆模块,分文件,对于这些组件内模块而言,入口文件的参数也就是全局数据。

这时个别有三种计划:

  1. props 透传。
  2. 上下文。
  3. 全局数据流。

props 透传计划,因为任何一个节点掉链子都会导致参数传递失败,因而带来的保护老本与心智累赘都特地大。

上下文即 useContext 利用上下文共享全局数据,带来的问题是更新粒度太粗,同上下文中任何值的扭转都会导致重渲染。有一种较为 Hack 的解决方案 use-context-selector,不过这个和上面说到的全局数据流很像。

全局数据流即利用 react-redux 等工具,绕过 React 更新机制进行全局数据传递的计划,这种计划较好解决了我的项目问题,但很少有组件会应用。以前也有过不少利用 Redux 做部分数据流的计划,但实质上还是全局数据流。当初 react-redux 反对了部分作用域计划:

import { shallowEqual, createSelectorHook, createStoreHook } from 'react-redux'const context = React.createContext(null)const useStore = createStoreHook(context)const useSelector = createSelectorHook(context)const useDispatch = createDispatchHook(context)

因而是机会好好梳理一下数据流治理计划,做一个我的项目、组件通用的数据流治理计划。

精读

对我的项目、组件来说,数据流蕴含两种数据:

  1. 可变数据。
  2. 不可变数据。

对我的项目来说,可变数据的起源有:

  1. 全局内部参数。
  2. 全局我的项目自定义变量。

不可变数据起源有:

  1. 操作数据或行为的函数办法。
全局内部参数指不受我的项目代码管制的,比方登陆用户信息数据。全局我的项目自定义变量是由我的项目代码管制的,比方定义了一些模型数据、状态数据。

对组件来说,可变数据的起源有:

  1. 组件被调用时的传参。
  2. 全局组件自定义变量。

不可变数据起源有:

  1. 组件被调用时的传参。
  2. 操作数据或行为的函数办法。

对组件来说,被调用时的传参既可能是可变数据,也可能是不可变数据。比方传入的 props.color 可能就是可变数据,而 props.defaultValueprops.onChange 就是不可变数据。

当梳理分明我的项目与组件到底有哪些全局数据后,咱们就能够依照注册与调用这两步来设计数据流治理标准了。

数据流调用

首先来看调用。为了同时保障应用的便捷与应用程序的性能,咱们心愿应用一个对立的 API useXXX 来拜访所有全局数据与办法,并满足:

  1. {} = useXXX() 只能援用到不可变数据,包含变量与办法。
  2. { value } = useXXX(state => ({ value: state.value })) 能够援用到可变数据,但必须通过选择器来调用。

比方一个利用叫 gaea,那么 useGaea 就是对这个利用全局数据的惟一调用入口,我能够在组件里这么调用数据与办法:

const Panel = () => {  // appId 是利用不可变数据,所以即便是变量也能够间接获取,因为它不会变动,也不会导致重渲染  // fetchData 是取数函数,内置发送了 appId,所以绑定了肯定上下文,也属于不可变数据  const { appId, fetchData } = useGaea()  // 主题色可能在运行时批改,只能通过选择器获取  // 此时这个组件会额定在 color 变动时重渲染  const { color } = useGaea(state => ({    color: state.theme?.color  }))}

比方一个组件叫 Menu,那么 useMenu 就是这个组件的全局数据调用入口,能够这么应用:

// SubMenu 是 Menu 组件的子组件,能够间接应用 useMenuconst SubMenu = () => {  // defaultValue 是一次性值,所以解决时做了不可变解决,这里曾经是不可变数据了  // onMenuClick 是回调函数,不论传参援用如何变动,这里都解决成不可变的援用  const { defaultValue, onMenuClick } = useMenu()  // disabled 是 menu 的参数,须要在变动时立刻响应,所以是可变数据  const { disabled } = useMenu(state => ({    disabled: state.disabled  }))  // selectedMenu 是 Menu 组件的外部状态,也作为可变数据调用  const { selectedMenu } = useMenu(state => ({    selectedMenu: state.selectedMenu  }))}

能够发现,在整个利用或者组件的应用 Scope 中,曾经做了一层形象,即不关怀数据是怎么来的,只关怀数据是否可变。这样对于组件或利用,随时能够将外部状态凋谢到 API 层,而外部代码齐全不必批改。

数据流注册

数据流注册的时候,咱们只有定义三种参数:

  1. dynamicValue: 动静参数,通过 useInput(state => state.xxx) 能力拜访到。
  2. staticValue: 动态参数,援用永远不会扭转,能够间接通过 useInput().xxx 拜访到。
  3. 自定义 hooks,入参是 staticValue getState setState,这里能够封装自定义办法,并且定义的办法都必须是动态的,能够间接通过 useInput().xxx 拜访到。
const { useState: useInput, Provider } = createHookStore<{  dynamicValue: {    fontSize: number  }  staticValue: {    onChange: (value: number) => void  }}>(({ staticValue }) => {  const onCustomChange = React.useCallback((value: number) => {    staticValue.onChange(value + 1)  }, [staticValue])  return React.useMemo(() => ({    onCustomChange  }), [onCustomChange])})

下面的办法裸露了 ProvideruseInput 两个对象,咱们首先须要在组件里给它传输数据。比方我写的是组件 Input,就能够这么调用:

function Input({ onChange, fontSize }) {  return (    <Provider dynamicValue={{fontSize}} staticValue={{onChange}}>      <InputComponent />    </Provider>  )}

如果对于某些动态数据,咱们只想赋初值,能够应用 defaultDynamicValue

function Input({ onChange, fontSize }) {  return (    <Provider dynamicValue={{fontSize}} defaultDynamicValue={{count: 1}}>      <InputComponent />    </Provider>  )}

这样 count 就是一个动静值,必须通过 useInput(state => ({ count: state.count })) 能力取到,但又不会因为外层组件 Rerender 而被从新赋值为 1。所有动静值都能够通过 setState 来批改,这个前面再说。

这样所有 Input 下的子组件就能够通过 useInput 拜访到全局数据流的数据啦,咱们有三种拜访数据的场景。

一:拜访传给 Input 组件的 onChange

因为 onChange 是不可变对象,因而能够通过如下形式拜访:

function InputComponent() {  const { onChange } = useInput()}

二:拜访咱们自定义的全局 Hooks 函数 onCustomChange

function InputComponent() {  const { onCustomChange } = useInput()}

三:拜访可能变动的数据 fontSize。因为咱们须要在 fontSize 变动时让组件重渲染,又不想让下面两种调用形式受到 fontSize 的影响,须要通过如下形式拜访:

function InputComponent() {  const { fontSize } = useInput(state => ({    fontSize: state.fontSize  }))}

最初在自定义办法中,如果咱们想批改可变数据,都要通过 updateStore 封装好并裸露给内部,而不能间接调用。具体形式是这样的,举个例子,假如咱们须要定义一个利用状态 status,其可选值为 editpreview,那么能够这么去定义:

const { useState: useInput, Provider } = createHookStore<{  dynamicValue: {    isAdmin: boolean    status: 'edit' | 'preview'  }}>(({ getState, setState }) => {  const toggleStatus = React.useCallback(() => {    // 管理员能力切换利用状态    if (!getState().isAdmin) {      return    }    setState(state => ({      ...state,      status: state.status === 'edit' ? 'preview' : 'edit'    }))  }, [getState, setState])  return React.useMemo(() => ({    toggleStatus  }), [toggleStatus])})

上面是调用:

function InputComponent() {  const { toggleStatus } = useInput()  return (    <button onClick={toggleStatus} />  )}

而且整个链路的类型定义也是齐全主动推导的,这套数据流治理计划到这里就讲完了。

总结

对全局数据的应用,最不便的就是收拢到一个 useXXX API,并且还能辨别动态、动静值,并在拜访动态值时齐全不会导致重渲染。

而之所以动静值 dynamicValue 须要在 Provider 里定义,是因为当动静值变动时,会自动更新数据流中的数据,使整个利用数据与内部动态数据同步。而这个更新步骤就是通过 Redux Store 来实现的。

本文特意没有给出实现源码,感兴趣的同学能够本人实现一个试一试。

探讨地址是:精读《一种 Hooks 数据流治理计划》· Issue #345 · dt-fe/weekly

如果你想参加探讨,请 点击这里,每周都有新的主题,周末或周一公布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

版权申明:自在转载-非商用-非衍生-放弃署名(创意共享 3.0 许可证)