总结自己使用过的Hooks数据流方式

6,357 阅读6分钟

Hooks正式推出也有一年出头了,总结这一年中,自己使用过的Hooks数据流方式

目前接触过的数据流方案大致是2种方式

  • 基于React的数据流
  • 不基于React的数据流

基于React的数据流实现

简单讲来,就是使用官方提供的Hooks,比如最常见的useState,这种方式也是我们用的最多的

useState

通过useState能将曾经类组件的state值拆分为多个

function App() {
  const [state, setState] = useState({ foo: 0 });
  const [loading, setLoading] = useState(false);
  // ...
}

使用useState最大的好处是简洁明了,个人觉得有2个缺点

  1. 缺点在于拆分过多,使用太多的useState后不好管理
  2. 如果某一个state值过于复杂,在改变值时的合并是比较难处理的

针对这两个小问题,也有另一个hooks解决

useReducer

useReducer可以说是官方简版redux

const reducer = (state, { type, loading }) => {
  if (type === "FOO") return { ...state, foo: 1 };
  if (type === "SET_LOADING") return { ...state, loading };
  return state;
};
function App() {
  const [state, dispatch] = useReducer(reducer, { foo: 0, loading: false });
  // ...
}

redux还有一些小差异的就是react提倡将初始值赋值在useReudcer的参数中,而不是reducerstate

上面2种方式在单组件或者是父子层级组件使用还是比较方便,如果想像redux一样不关乎层级共享数据呢?在Hooks中也提供了对应的方法

useContext + useReducer

这种方式是我在这一年中使用最多的跨组件共享状态的方式了

const Context = createContext({});
const reducer = (state, { type, loading }) => {
  if (type === "FOO") return { ...state, foo: 1 };
  if (type === "SET_LOADING") return { ...state, loading };
  return state;
};
function App() {
  const [state, dispatch] = useReducer(reducer, { foo: 0, loading: false });
  return (
    <Context.Provider value={{ state, dispatch }}>
      // ...children
    </Context.Provider>
  );
}
function Foo() {
  const { state, dispatch } = useContext(Context);
  // ...
}

这种方式能将自己的hooks跨组件共享状态了,使用还是比较方便,唯一的缺点就是自己需要使用createContext来创建Context并且挂载Provider,会多一点步骤,也需要自己管理Context,这可以说是纯手动

unstated-next

unstated-next是一个200 字节的状态管理解决方案

function useCounter(initialState = 0) {
  const [count, setCount] = useState(initialState)
  return { count, setCount }
}

const Counter = createContainer(useCounter);

function Foo() {
  const counter = Counter.useContainer();
  // ...
}

function App() {
  return (
    <Counter.Provider>
      <Foo />
    </Counter.Provider>
  )
}

unstated-next的实现使用的全是Reactapi,源码也短,下面会对他的源码进行分析

使用unstated-next让我们不需要自己创建和管理Context了,从纯手动切换到了半自动

UmiJS中提供的useModel

UmiJS中提供了一个useModel方法,可以很方便的将hooks全局使用,它的默认规则是src/models下导出的hooks会作用于全局

// src/models/count.ts
export default () => {
  const [count, setCount] = useState(0);
  return { count, setCount };
};
// 
function App() {
  const { count } = useModel('count');
  // ...
}

useModel等于是省去了上面的useContext和挂载<Prvoider>的步骤,由框架处理了,在React Developer Tools可以看到最外层是有一个存了导出hooks的值的Provider的,我想他的实现方式应该和unstated-next类似

从自己创建和管理Context,挂载Provider,到自己挂载Provder,再到只需要写hooks逻辑,过程就是手动——半自动——全自动

它的缺点就是范围局限了,仅限于UmiJS框架

基于React的数据流优缺点

仅在使用过程中个人的总结

优点

  • 基于React,没有额外的学习过程,简单易用

缺点

  • 无法做到精确刷新
  • 只是简单是数据流管理,并不包含常见的异步数据的处理

Context无法做到精确刷新

数据是一个整体,不能做到精确刷新,一旦改变React就会自动触发刷新

function Foo() {
  const { state: { foo } } = useContext(Context);
  // ...
}
function Bar() {
  const { state: { bar } } = useContext(Context);
  // ...
}

如果在<Foo>中调用了dispatch()state.foo进行了更改,<Bar>也会刷新

不基于React的数据流实现

不基于React的数据流实现就是数据存储不在React里,数据改变不会直接触发组件刷新,而是通过其他的方式触发组件重新渲染,我只使用过2种

  • Redux+React-Redux
  • DvaJs

React-Redux

Hooks推出后,React-Redux也更新了Hooks方法,使用useSelector()来取得state

const Counter = () => {
  const counter = useSelector(state => state.counter)
  // ...
}

它的优点是会精确刷新,不会像Context一样导致整体刷新,因为useSelector的重新渲染是自己控制的,而不是交给React处理

Redux+React-Redux的缺点我想用过的都知道,就是需要管理很多文件

DvaJS

DvaJS其实没有提供Hooks的数据流方式

DvaJS单独使用很少,基本是使用UmiJS,实际在Umi中有Hooks的方式去获取数据

云谦大佬可能全身心投入UmiJS开发,已经有很长一段时间没更新了,但是我觉得作为一个集成度非常高的优秀数据流管理。

仅仅使用过2个月的我对DvaJS的总结

优点

  • 集成度很高,约定式,使用了redux-sage解决异步数据流问题
  • redux+react-redux简单很多,不再会有文件管理问题
  • 约定式的动态增加reducer,让一些数据可以懒加载

小小的总结

在类组件时代,无法拆分state,类组件感觉就很重,组件级state多了也很难管理,Context流行度也不算高,使用<Context.Consumer>让组件更重了,后来有了contextType也没有useContext这么简洁,所以感觉之前流行Redux也是有原因的,因为React本身没有提供好的状态管理

Hooks时代,一切都变得更简洁,官方提供了useReducer这样的简洁版Redux,同时组件级的state也更简单,使用自定义的Hooks,让数据和视图耦合更低了,useContext+useReducer的方案可以解决大部分需要共享状态的场景

使用了挺久的React,我感觉很多场景都不会全局共享状态,我现在做的项目就是后台管理系统,页面的数据也不会和别的页面关联,我都使用Context一把梭了,所以我更喜欢轻量级的Hooks数据流解决方案

看看unstated-next源码

有时候感觉自己真的变成了搬运工,缺乏自己的想法,就很呆,上面提到,我很长一段时间都是用useContext来共享状态,每次都是手动挡,前几天突然想,为什么自己要做重复的工作,一搜果然有大佬已经写好了

// Provider传入的Props
export interface ContainerProviderProps<State = void> {
  initialState?: State;
  children: React.ReactNode;
}
// createContainer创建的Container类型
export interface Container<Value, State = void> {
  Provider: React.ComponentType<ContainerProviderProps<State>>;
  useContainer: () => Value;
}

// 创建一个Container
export function createContainer<Value, State = void>(
  // 自定义数据的hook
  useHook: (initialState?: State) => Value
): Container<Value, State> {
  // Context用来传递数据
  let Context = React.createContext<Value | null>(null);

  function Provider(props: ContainerProviderProps<State>) {
    // 用初始数据初始化自定义的hook
    let value = useHook(props.initialState);
    // 将hook的返回值赋值给Provider
    return <Context.Provider value={value}>{props.children}</Context.Provider>;
  }

  // 使用Container,值就是自定义的hook的返回值
  function useContainer(): Value {
    let value = React.useContext(Context);
    if (value === null) {
      throw new Error("Component must be wrapped with <Container.Provider>");
    }
    return value;
  }

  return { Provider, useContainer };
}

export function useContainer<Value, State = void>(
  container: Container<Value, State>
): Value {
  return container.useContainer();
}

源码很简单,就是一个闭包

作者说 “我相信 React 在状态管理方面已经非常出色”、“我希望社区放弃像 Redux 这样的状态管理库,并找到使用 React 内置工具链的更好方法”,我觉得很有道理

虽然感觉这篇没有什么技术含量,但是没有灵感,也有三周没有分享文章了


最后,祝大家身体健康,工作顺利!

欢迎大家关注我的公众号~


参考文章:精读《React Hooks 数据流》