4

When maintaining a large-scale project OR UI component module, you will definitely encounter global data transfer problems.

When maintaining a project, such as global user information, global project configuration, global function configuration, etc., are global data reused across modules.

When maintaining a UI component, there is only one entry for calling the component, but the module will continue to be disassembled and filed within the component. For these modules in the component, the parameters of the entry file are global data.

There are generally three options at this time:

  1. Props pass through.
  2. Context.
  3. Global data flow.

The props transparent transmission scheme, because any node off the chain will cause parameter transmission failure, so the maintenance cost and mental burden are particularly large.

The context is useContext using context to share global data. The problem is that the update granularity is too coarse, and any value change in the same context will cause re-rendering. There is a more Hack solution use-context-selector , but this is very similar to the global data flow mentioned below.

Global data flow is a solution that uses react-redux bypass the React update mechanism for global data transfer. This solution better solves the project problem, but few components will use it. There have been many schemes using Redux for local data flow before, but it is essentially a global data flow. Now react-redux supports the local scope scheme:

import { shallowEqual, createSelectorHook, createStoreHook } from 'react-redux'

const context = React.createContext(null)
const useStore = createStoreHook(context)
const useSelector = createSelectorHook(context)
const useDispatch = createDispatchHook(context)

Therefore, it is an opportunity to sort out the data flow management plan and make a data flow management plan that is common to projects and components.

intensive reading

For projects and components, the data stream contains two types of data:

  1. Variable data.
  2. Immutable data.

For the project, the sources of variable data are:

  1. Global external parameters.
  2. Global project custom variables.

Sources of immutable data are:

  1. Functional methods for manipulating data or behavior.
Global external parameters refer to those that are not controlled by the project code, such as login user information data. The global project custom variables are controlled by the project code, for example, some model data and status data are defined.

For components, the sources of variable data are:

  1. Passing parameters when the component is called.
  2. Global component custom variables.

Sources of immutable data are:

  1. Passing parameters when the component is called.
  2. Functional methods for manipulating data or behavior.

For components, the parameters passed when called may be either variable data or immutable data. For example, the incoming props.color may be variable data, while props.defaultValue and props.onChange are immutable data.

After sorting out the global data of the project and the component, we can design the data flow management specification according to the two steps of registration and invocation.

Data flow call

First look at the call. In order to ensure the convenience of use and the performance of the application at the same time, we hope to use a unified API useXXX to access all global data and methods, and meet:

  1. {} = useXXX() can only refer to immutable data, including variables and methods.
  2. { value } = useXXX(state => ({ value: state.value })) can refer to variable data, but it must be called through a selector.

For example, if an application is called gaea useGaea is the only call entry for the global data of this application. I can call the data and methods in the component like this:

const Panel = () => {
  // appId 是应用不可变数据,所以即使是变量也可以直接获取,因为它不会变化,也不会导致重渲染
  // fetchData 是取数函数,内置发送了 appId,所以绑定了一定上下文,也属于不可变数据
  const { appId, fetchData } = useGaea()

  // 主题色可能在运行时修改,只能通过选择器获取
  // 此时这个组件会额外在 color 变化时重渲染
  const { color } = useGaea(state => ({
    color: state.theme?.color
  }))
}

For example, if a component is called Menu useMenu is the global data call entry of this component. It can be used like this:

// SubMenu 是 Menu 组件的子组件,可以直接使用 useMenu
const 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
  }))
}

It can be found that in the scope of use of the entire application or component, an abstraction has been made, that is, it does not care how the data comes, but only cares about whether the data is variable. In this way, for components or applications, the internal state can be opened to the API layer at any time, and the internal code does not need to be modified at all.

Data flow registration

When registering a data stream, we only need to define three parameters:

  1. dynamicValue : Dynamic parameters, useInput(state => state.xxx) only through 06128590856264.
  2. staticValue : Static parameters, the reference will never change, you can directly access it useInput().xxx
  3. Custom hooks, the staticValue getState setState , here you can encapsulate custom methods, and the defined methods must be static, which can be accessed directly through 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])
})

The above method exposes Provider and useInput , we first need to transmit data to it in the component. For example, I wrote the component Input , so I can call it like this:

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

If we only want to assign initial values to some dynamic data, we can use defaultDynamicValue :

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

In this way, count is a dynamic value, which must be obtained through useInput(state => ({ count: state.count })) , but it will not be re-assigned to 1 because of the outer component Renderer. All dynamic values can be setState , which will be discussed later.

In this way, all sub-components under Input can useInput . We have three scenarios for accessing data.

I: Access pass Input components onChange .

Because onChange is an immutable object, it can be accessed as follows:

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

2: Visit our customized global Hooks function onCustomChange :

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

Three: Access the data that may change fontSize . Since we need fontSize changes, and we don't want the above two calling methods to be affected by fontSize , we need to access it in the following ways:

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

Finally, in the custom method, if we want to modify the variable data, we must updateStore and expose it to the outside, instead of calling it directly. status . For example, suppose we need to define an application state 06128590856477 with optional values of edit and preview , then we can define it like this:

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])
})

Here is the call:

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

  return (
    <button onClick={toggleStatus} />
  )
}

Moreover, the type definition of the entire link is fully automatically deduced, and this set of data flow management solutions is finished here.

Summarize

For the use of global data, the most convenient way is to gather a useXXX API, and can distinguish static and dynamic values, and will not cause re-rendering when accessing static values.

The reason why the dynamic value dynamicValue needs to be Provider is because when the dynamic value changes, the data in the data stream will be automatically updated to synchronize the entire application data with external dynamic data. And this update step is done through the Redux Store.

This article deliberately does not give the implementation source code, and interested students can try it out by themselves.

The discussion address is: Intensive Reading of "A Hooks Data Flow Management Scheme" · Issue #345 · dt-fe/weekly

If you want to participate in the discussion, please Click here , a new theme every week, weekend or Monday. Front-end intensive reading-to help you filter reliable content.

Follow front-end intensive reading WeChat public number

Copyright notice: Freely reprinted-non-commercial-non-derivative-keep the signature ( Creative Commons 3.0 License )

黄子毅
7k 声望9.5k 粉丝