基于React Hooks的小型状态管理详解

目录
  • 实现基于 React Hooks 的状态共享
  • 使用感受

本文主要介绍一种基于 React Hooks 的状态共享方案,介绍其实现,并总结一下使用感受,目的是在状态管理方面提供多一种选择方式。

实现基于 React Hooks 的状态共享

React 组件间的状态共享,是一个老生常谈的问题,也有很多解决方案,例如 Redux、MobX 等。这些方案很专业,也经历了时间的考验,但私以为他们不太适合一些不算复杂的项目,反而会引入一些额外的复杂度。

实际上很多时候,我不想定义 mutation 和 action、我不想套一层 context,更不想写 connect 和 mapStateToProps;我想要的是一种轻量、简单的状态共享方案,简简单单引用、简简单单使用。

随着 Hooks 的诞生、流行,我的想法得以如愿。

接着介绍一下我目前在用的方案,将 Hooks 与发布/订阅模式结合,就能实现一种简单、实用的状态共享方案。因为代码不多,下面将给出完整的实现。

import {
  Dispatch,
  SetStateAction,
  useCallback,
  useEffect,
  useReducer,
  useRef,
  useState,
} from 'react';

/**
 * @see https://github.com/facebook/react/blob/bb88ce95a87934a655ef842af776c164391131ac/packages/shared/objectIs.js
 * inlined Object.is polyfill to avoid requiring consumers ship their own
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
 */
function is(x: any, y: any): boolean {
  return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
}

const objectIs = typeof Object.is === 'function' ? Object.is : is;

/**
 * @see https://github.com/facebook/react/blob/933880b4544a83ce54c8a47f348effe725a58843/packages/shared/shallowEqual.js
 * Performs equality by iterating through keys on an object and returning false
 * when any key has values which are not strictly equal between the arguments.
 * Returns true when the values of all keys are strictly equal.
 */
function shallowEqual(objA: any, objB: any): boolean {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    if (
      !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}

const useForceUpdate = () => useReducer(() => ({}), {})[1] as VoidFunction;

type ISubscriber<T> = (prevState: T, nextState: T) => void;

export interface ISharedState<T> {
  /** 静态方式获取数据, 适合在非组件中或者数据无绑定视图的情况下使用 */
  get: () => T;
  /** 修改数据,赋予新值 */
  set: Dispatch<SetStateAction<T>>;
  /** (浅)合并更新数据 */
  update: Dispatch<Partial<T>>;
  /** hooks方式获取数据, 适合在组件中使用, 数据变更时会自动重渲染该组件 */
  use: () => T;
  /** 订阅数据的变更 */
  subscribe: (cb: ISubscriber<T>) => () => void;
  /** 取消订阅数据的变更 */
  unsubscribe: (cb: ISubscriber<T>) => void;
  /** 筛出部分 state */
  usePick<R>(picker: (state: T) => R, deps?: readonly any[]): R;
}

export type IReadonlyState<T> = Omit<ISharedState<T>, 'set' | 'update'>;

/**
 * 创建不同实例之间可以共享的状态
 * @param initialState 初始数据
 */
export const createSharedState = <T>(initialState: T): ISharedState<T> => {
  let state = initialState;
  const subscribers: ISubscriber<T>[] = [];

  // 订阅 state 的变化
  const subscribe = (subscriber: ISubscriber<T>) => {
    subscribers.push(subscriber);
    return () => unsubscribe(subscriber);
  };

  // 取消订阅 state 的变化
  const unsubscribe = (subscriber: ISubscriber<T>) => {
    const index = subscribers.indexOf(subscriber);
    index > -1 && subscribers.splice(index, 1);
  };

  // 获取当前最新的 state
  const get = () => state;

  // 变更 state
  const set = (next: SetStateAction<T>) => {
    const prevState = state;
    // @ts-ignore
    const nextState = typeof next === 'function' ? next(prevState) : next;
    if (objectIs(state, nextState)) {
      return;
    }
    state = nextState;
    subscribers.forEach((cb) => cb(prevState, state));
  };

  // 获取当前最新的 state 的 hooks 用法
  const use = () => {
    const forceUpdate = useForceUpdate();

    useEffect(() => {
      let isMounted = true;
      // 组件挂载后立即更新一次, 避免无法使用到第一次更新数据
      forceUpdate();
      const un = subscribe(() => {
        if (!isMounted) return;
        forceUpdate();
      });
      return () => {
        un();
        isMounted = false;
      };
    }, []);

    return state;
  };

  const usePick = <R>(picker: (s: T) => R, deps = []) => {
    const ref = useRef<any>({});

    ref.current.picker = picker;

    const [pickedState, setPickedState] = useState<R>(() =>
      ref.current.picker(state),
    );

    ref.current.oldState = pickedState;

    const sub = useCallback(() => {
      const pickedOld = ref.current.oldState;
      const pickedNew = ref.current.picker(state);
      if (!shallowEqual(pickedOld, pickedNew)) {
        // 避免 pickedNew 是一个 function
        setPickedState(() => pickedNew);
      }
    }, []);

    useEffect(() => {
      const un = subscribe(sub);
      return un;
    }, []);

    useEffect(() => {
      sub();
    }, [...deps]);

    return pickedState;
  };

  return {
    get,
    set,
    update: (input: Partial<T>) => {
      set((pre) => ({
        ...pre,
        ...input,
      }));
    },
    use,
    subscribe,
    unsubscribe,
    usePick,
  };
};

拥有 createSharedState 之后,下一步就能轻易地创建出一个可共享的状态了,在组件中使用的方式也很直接。

// 创建一个状态实例
const countState = createSharedState(0);

const A = () => {
  // 在组件中使用 hooks 方式获取响应式数据
  const count = countState.use();
  return <div>A: {count}</div>;
};

const B = () => {
  // 使用 set 方法修改数据
  return <button onClick={() => countState.set(count + 1)}>Add</button>;
};

const C = () => {
  return (
    <button
      onClick={() => {
        // 使用 get 方法获取数据
        console.log(countState.get());
      }}
    >
      Get
    </button>
  );
};

const App = () => {
  return (
    <>
      <A />
      <B />
      <C />
    </>
  );
};

对于复杂对象,还提供了一种方式,用于在组件中监听指定部分的数据变化,避免其他字段变更造成多余的 render:

const complexState = createSharedState({
  a: 0,
  b: {
    c: 0,
  },
});

const A = () => {
  const a = complexState.usePick((state) => state.a);
  return <div>A: {a}</div>;
};

但复杂对象一般更建议使用组合派生的方式,由多个简单的状态派生出一个复杂的对象。另外在有些时候,我们会需要一种基于原数据的计算结果,所以这里同时提供了一种派生数据的方式。

通过显示声明依赖的方式监听数据源,再传入计算函数,那么就能得到一个响应式的派生结果了。

/**
 * 状态派生(或 computed)
 * ```ts
 * const count1 = createSharedState(1);
 * const count2 = createSharedState(2);
 * const count3 = createDerivedState([count1, count2], ([n1, n2]) => n1 + n2);
 * ```
 * @param stores
 * @param fn
 * @param initialValue
 * @returns
 */
export function createDerivedState<T = any>(
  stores: IReadonlyState<any>[],
  fn: (values: any[]) => T,
  opts?: {
    /**
     * 是否同步响应
     * @default false
     */
    sync?: boolean;
  },
): IReadonlyState<T> & {
  stop: () => void;
} {
  const { sync } = { sync: false, ...opts };
  let values: any[] = stores.map((it) => it.get());
  const innerModel = createSharedState<T>(fn(values));

  let promise: Promise<void> | null = null;

  const uns = stores.map((it, i) => {
    return it.subscribe((_old, newValue) => {
      values[i] = newValue;

      if (sync) {
        innerModel.set(() => fn(values));
        return;
      }

      // 异步更新
      promise =
        promise ||
        Promise.resolve().then(() => {
          innerModel.set(() => fn(values));
          promise = null;
        });
    });
  });

  return {
    get: innerModel.get,
    use: innerModel.use,
    subscribe: innerModel.subscribe,
    unsubscribe: innerModel.unsubscribe,
    usePick: innerModel.usePick,
    stop: () => {
      uns.forEach((un) => un());
    },
  };
}

至此,基于 Hooks 的状态共享方的实现介绍就结束了。

在最近的项目中,有需要状态共享的场景,我都选择了上述方式,在 Web 项目和小程序 Taro 项目中均能使用同一套实现,一直都比较顺利。

使用感受

最后总结一下目前这种方式的几个特点:

1.实现简单,不引入其他概念,仅在 Hooks 的基础上结合发布/订阅模式,类 React 的场景都能使用,比如 Taro;

2.使用简单,因为没有其他概念,直接调用 create 方法即可得到 state 的引用,调用 state 实例上的 use 方法即完成了组件和数据的绑定;

3.类型友好,创建 state 时无需定义多余的类型,使用的时候也能较好地自动推导出类型;

4.避免了 Hooks 的“闭包陷阱”,因为 state 的引用是恒定的,通过 state 的 get 方法总是能获取到最新的值:

const countState = createSharedState(0);

const App = () => {
  useEffect(() => {
    setInterval(() => {
      console.log(countState.get());
    }, 1000);
  }, []);
  // return ...
};

5.直接支持在多个 React 应用之间共享,在使用一些弹框的时候是比较容易出现多个 React 应用的场景:

const countState = createSharedState(0);

const Content = () => {
  const count = countState.use();
  return <div>{count}</div>;
};

const A = () => (
  <button
    onClick={() => {
      Dialog.info({
        title: 'Alert',
        content: <Content />,
      });
    }}
  >
    open
  </button>
);

6.支持在组件外的场景获取/更新数据

7.在 SSR 的场景有较大局限性:state 是细碎、分散创建的,而且 state 的生命周期不是跟随 React 应用,导致无法用同构的方式编写 SSR 应用代码

以上,便是本文的全部内容,实际上 Hooks 到目前流行了这么久,社区当中已有不少新型的状态共享实现方式,这里仅作为一种参考。

根据以上特点,这种方式有明显的优点,也有致命的缺陷(对于 SSR 而言),但在实际使用中,可以根据具体的情况来选择合适的方式。比如在 Taro2 的小程序应用中,无需关心 SSR,那么我更倾向于这种方式;如果在 SSR 的同构项目中,那么定还是老老实实选择 Redux。

总之,是多了一种选择,到底怎么用还得视具体情况而定。 

以上就是基于React Hooks的小型状态管理详解的详细内容,更多关于React Hooks 小型状态管理的资料请关注我们其它相关文章!

(0)

相关推荐

  • React Hooks常用场景的使用(小结)

    前言 React 在 v16.8 的版本中推出了 React Hooks 新特性.在我看来,使用 React Hooks 相比于从前的类组件有以下几点好处: 代码可读性更强,原本同一块功能的代码逻辑被拆分在了不同的生命周期函数中,容易使开发者不利于维护和迭代,通过 React Hooks 可以将功能代码聚合,方便阅读维护: 组件树层级变浅,在原本的代码中,我们经常使用 HOC/render props 等方式来复用组件的状态,增强功能等,无疑增加了组件树层数及渲染,而在 React Hooks

  • react hooks入门详细教程

    State Hooks 案例: import { useState } from 'react'; function Example() { const [count, setCount] = useState(0); //count:声明的变量:setCount:改变count值的函数:0:count的初始值 return ( <div> <p>You clicked {count} times</p> <button onClick={() => set

  • React之Hooks详解

    目录 什么是钩子(hooks) 类组件 函数组件 为什么创造Hooks 总结 什么是钩子(hooks) 消息处理的一种方法, 用来监视指定程序 函数组件中需要处理副作用,可以用钩子把外部代码"钩"进来 常用钩子:useState, useEffect, useContext, useReducer Hooks一律使用use前缀命名:useXXX 类组件 函数组件 一类特殊的函数,为你的函数式组件注入特殊的功能 为什么创造Hooks 有些类组件冗长且复杂,难以复用 结局方案:无状态组件与

  • 教你在react中创建自定义hooks

    一.什么是自定义hooks 逻辑复用 简单来说就是使用自定义hook可以将某些组件逻辑提取到可重用的函数中. 自定义hook是一个从use开始的调用其他hook的Javascript函数. 二.不使用自定义hook时 例1:当我们整个页面需要获取用户鼠标移动的坐标时,不使用hook的代码,我们可以这样写 const [position, setPosition] = useState({ x: 0, y: 0 }) useEffect(() => { const move = (e) => {

  • React 使用Hooks简化受控组件的状态绑定

    开始之前 阅读本文需要对以下几项有一定了解 ECMAScript 6 文章中大量用到了 ES6 语法,比如解构赋值和函数参数默认值.剩余参数.展开语法.箭头函数等. Hooks React 在 16.8 版本中推出了 Hooks,它允许你在"函数组件"中使用"类组件"的一些特性. React 本身提供了一些 Hooks,比如 useState.useReducer 等.通过在一个以"use"作为命名起始的函数中调用这些 Hooks,就得到了一个

  • react的hooks的用法详解

    hooks的作用 它改变了原始的React类的开发方式,改用了函数形式;它改变了复杂的状态操作形式,让程序员用起来更轻松;它改变了一个状态组件的复用性,让组件的复用性大大增加. useState // 声明状态 const [ count , setCount ] = useState(0); // 使用状态 <p>You clicked {count} times</p> <button onClick={()=>{setCount(count+1)}}>cli

  • React hooks的优缺点详解

    前言 Hook 是 React 16.8 的新增特性.它是完全可选的,并且100%向后兼容.它可以让你使用函数组件的方式,运用类组件以及 react 其他的一些特性,比如管理状态.生命周期钩子等.从概念上讲,React 组件一直更像是函数.而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则. 优点: 1.代码可读性更强,原本同一块功能的代码逻辑被拆分在了不同的生命周期函数中,容易使开发者不利于维护和迭代,通过 React Hooks 可以将功能代码聚合,方便阅读维护.例如,每个生

  • React Hooks使用常见的坑

    React Hooks 是 React 16.8 引入的新特性,允许我们在不使用 Class 的前提下使用 state 和其他特性.React Hooks 要解决的问题是状态共享,是继 render-props 和 higher-order components 之后的第三种状态逻辑复用方案,不会产生 JSX 嵌套地狱问题. 为什么会有Hooks? 介绍Hooks之前,首先要给大家说一下React的组件创建方式,一种是类组件,一种是纯函数组件,并且React团队希望,组件不要变成复杂的容器,最好

  • 基于React Hooks的小型状态管理详解

    目录 实现基于 React Hooks 的状态共享 使用感受 本文主要介绍一种基于 React Hooks 的状态共享方案,介绍其实现,并总结一下使用感受,目的是在状态管理方面提供多一种选择方式. 实现基于 React Hooks 的状态共享 React 组件间的状态共享,是一个老生常谈的问题,也有很多解决方案,例如 Redux.MobX 等.这些方案很专业,也经历了时间的考验,但私以为他们不太适合一些不算复杂的项目,反而会引入一些额外的复杂度. 实际上很多时候,我不想定义 mutation 和

  • flutter自定义InheritedProvider实现状态管理详解

    目录 InheritedWidget简单数据驱动模型 1. 数据存储 2. 变更通知 3. 使用方法 InheritedWidget简单数据驱动模型 基于InheritedWidget,实现简单的数据驱动模型,模型结构如下: 1. 数据存储 使用 InheritedWidget,新建 InheritedProvider import 'package:flutter/material.dart'; class InheritedProvider<T> extends InheritedWidg

  • vue前端开发辅助函数状态管理详解示例

    目录 mapState mapGetters mapMutations mapActions 示例 小结 mapState 当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余.为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性.不使用mapState时,获取对象状态,通常放在使用组件的computes属性中,使用方式为: //.... computed: { count: function(){ return this.$store.state

  • Mobx实现React 应用的状态管理详解

    目录 MobX 从一个 demo 开始 创建类并将其转化成可观察对象 使用可观察对象 MobX 与 React 集成 在组件中使用可观察对象 1. 访问全局的类实例 2. 通过 props 3. 通过 React Context 4. 在组件中实例化 observable class 并存储它的实例 5. 在组件中调用 observable 方法创建可观察对象 6. 在函数组件中使用 useLocalObservable 让组件具备观察能力 总结 MobX MobX 是一个状态管理库,它会自动收

  • javascript Redux的状态管理详解

    所谓的状态管理,就是对应用程序中的数据进行管理. 理念:凡是数据流管理混乱的项目,几乎都上不了线.好的项目,必须有非常良好的数据流管理. 如何使用Redux?记住“3个3”. 第1个三:3个api,createStore.combineReducers.applyMiddleware 第2个三:3个特点,store单一数据源.store只读的.只能通过reducer纯函数来修改store. 第3个三:3个概念,store.action.reducer. 基本概念: state 包含所有数据,用来

  • vue3如何使用provide实现状态管理详解

    目录 前言 如何通过 provide/inject 实现 Vuex的功能 在应用中注册此插件 插件的入口文件 创建 store ,把对应的数据挂载到根组件上 实现 mapState.mapMutations 和 mapActions方法 组件中使用 总结 前言 在 Vue 生态中, Vuex 这个官方的状态管理库在 Vue 应用开发中,为我们带来了非常便捷的功能.但是 Vuex 20K+ 的大小,也带来了一些成本,对于项目规模较小的应用来说, 引入 Vuex 只是为了存储用户信息之类的一小撮数据

  • vue之使用vuex进行状态管理详解

    目录 vuex进行状态管理 vuex状态管理基本使用 vuex进行状态管理 首先学习vuex必须先知道vue原理 Vue是一个典型的MVVM框架,模型(Model)只是普通的JavaScript对象,修改它则视图(View)会自动更新.这种设计让状态管理变得非常简单而直观 Vue实现这种数据双向绑定的效果,需要三大模块: Observer:能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者 Compile:对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更

  • SpringCloud微服务开发基于RocketMQ实现分布式事务管理详解

    目录 消息队列实现分布式事务原理 RocketMQ的事务消息 代码实现 基础配置 发送半消息 执行本地事务与回查 Account-Service消费消息 测试 小结 消息队列实现分布式事务原理 首先让我们来看一下基于消息队列实现分布式事务的原理方案. 柔性事务 发送消息的服务有个OUTBOX数据表,在进行INSERT.UPDATE.DELETE 业务操作时也会给OUTBOX数据表INSERT一条消息记录,这样可以保证原子性,因为这是基于本地的ACID事务. OUTBOX表充当临时消息队列,然后我

  • 基于react hooks,zarm组件库配置开发h5表单页面的实例代码

    最近使用React Hooks结合zarm组件库,基于js对象配置方式开发了大量的h5表单页面.大家都知道h5表单功能无非就是表单数据的收集,验证,提交,回显编辑,通常排列方式也是自上向下一行一列的方式显示 , 所以一开始就考虑封装一个配置化的页面生成方案,目前已经有多个项目基于此方式配置开发上线,思路和实现分享一下. 使用场景 任意包含表单的h5页面(使用zarm库,或自行适配自己的库) 目标 代码实现简单和简洁 基于配置 新手上手快,无学习成本 老手易扩展和维护 写之前参考了市面上的一些方案

  • React Context源码实现原理详解

    目录 什么是 Context Context 使用示例 createContext Context 的设计非常特别 useContext useContext 相关源码 debugger 查看调用栈 什么是 Context 目前来看 Context 是一个非常强大但是很多时候不会直接使用的 api.大多数项目不会直接使用 createContext 然后向下面传递数据,而是采用第三方库(react-redux). 想想项目中是不是经常会用到 @connect(...)(Comp) 以及 <Pro

随机推荐