【译】React全局状态用作本地状态

hello大家好,今天的译文由团队金牌导师 uncle13 带来。在react里,我们推崇单向数据流(所谓的 single source of truth),这就要求我们把全局共享的数据放在一个公共的地方,比如 redux 之类的。但是你知道的,redux用起来稍微有点麻烦,小项目要用它,真的是有点杀鸡用牛刀了。
今天的文章里,作者分享了一种利用 useState 以及 订阅发布模式,来设计简单的 响应式的全局数据,感兴趣的小伙伴可以看看。
原文地址:https://webup.org/blog/react-global-state-as-local-state/
下面是正文部分。

React全局状态访问

在React中,如何像处理本地状态一样处理全局状态(useState)?是否有一个方便的useGlobalState钩子可以用来读取全局存储对象,并在它更新时强制重新渲染所有订阅的组件,而不需要冗余的代码呢?没有Redux或Context参与,就这么简单。

一个用于组件的React钩子:

function Profile({
  const [user] = useGlobalState("user");

  return (
    <div className="Profile">
      <h3>{user.name}</h3>
      <p>{user.age}</p>
    </div>

  );
}

与useState相同的API — const [data, setData] = useGlobalState(storeSlice);

也可以在React组件外部访问全局状态:

// 在React组件外部访问全局存储

import { setGlobalState, getGlobalState } from "../store.config.js";

getGlobalState("user");

setGlobalState("user", {
  name"Johny Bo",
  age23,
});

以上代码的存储配置非常简单。没有Redux-like的reducer、selector或action。只需要一个.js/ts配置文件(注意store.config.js的导入)来初始化存储。

// store.config.js
// 初始化全局存储

import createGlobalState from "./createGlobalState.js";

const initialState = {
  user: {
    name"Justin Case",
    age37,
  },
};

export const { useGlobalState, setGlobalState, getGlobalState } =
  createGlobalState(initialState);

你只需调用createGlobalState()并传入初始存储值,然后导出钩子和它返回的两个函数即可。

然后,在你的代码库中的任何地方/组件都可以从相同的配置导入这些函数来获取/设置存储数据。

import {
  useGlobalState,
  setGlobalState,
  getGlobalState,
from "../store.config.js";

全局存储(发布/订阅)实现

createGlobalState()的JavaScript实现代码约为60行(不包括注释)。它具有从组件内外部获取和设置存储值的功能,并处理与特定存储片段“挂钩”的所有组件的更新(重新渲染)。

 createGlobalState.js :

// createGlobalState.js

import { useState, useEffect } from "react";

function createGlobalState(initialState = {}{
 const state = initialState;
 const subscribers = {};

 // Subscribers per state slice/prop
 // e.g. state.user
 for (const i in state) {
   subscribers[i] = [];
 }

 function useGlobalState(key{
   // To prevent getting/setting keys that aren't initialized
   // May go away with the TypeScript implementation
   if (!state.hasOwnProperty(key)) {
     throw new Error("This key does not exist in the store");
   }

   // Global state as React local state
   const [stateChunk, setStateChunk] = useState(state[key]);

   useEffect(() => {
     subscribers[key].push(setStateChunk);

     // Cleanup: subscriber removal after effect execution
     return () => {
       const index = subscribers[key].findIndex((fn) => fn === setStateChunk);
       subscribers[key].splice(index, 1);
     };
   }, [key]);
   return [
     stateChunk,
     (value) => {
       setGlobalState(key, value);
     },
   ];
 }

 // Useful for setting state outside React components
 const setGlobalState = (key, value = null) => {
   // To prevent setting keys that aren't initialized
   if (!state.hasOwnProperty(key)) {
     throw new Error("This key does not exist in the store");
   }

   state[key] = value;

   subscribers[key].forEach((subscriber) => {
     subscriber(value);
   });
 };

 // Useful for getting state outside React components
 const getGlobalState = (key) => {
   // To prevent getting keys that aren't initialized
   if (!state.hasOwnProperty(key)) {
     throw new Error("This key does not exist in the store");
   }
   return state[key];
 };

 return {
   useGlobalState,
   setGlobalState,
   getGlobalState,
 };
}

export default createGlobalState;

在处理钩子时,你可以确保存储配置文件和组件内部的类型安全:

// store.config.ts

import createGlobalState from "./createGlobalState";

export type User = {
  name: string;
  age: number;
};

type State = {
  user: User | null;
};

export const initialState: State = {
  user: {
    name: "Justin Case",
    age: 37,
  },
};

export const { useGlobalState, setGlobalState, getGlobalState } =
  createGlobalState<State>(initialState);

组件中的类型安全:

// Profile.tsx

import { useGlobalState, type User } from "../store.config.js";

function Profile({
  const [user, setUser] = useGlobalState<User>("user");
  const [name, setName] = useState(user?.name);
  const [age, setAge] = useState(user?.age);

  const handleSubmit = () => {
    setUser({ name, age });
  };

  return (
    <form
      onSubmit={() => {
        setUser({ name, age });
      }}
    >
      <input
        type="text"
        name="name"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <input
        type="text"
        name="age"
        value={age}
        onChange={(e) => setAge(e.target.value)}
      />
    </form>
  );
}


下面的示例(codesandbox)展示了如何使用useGlobalState来获取和更新存储的一部分,并强制所有订阅的组件在更改时重新渲染。

在实验中没有包括,但使用setGlobalState(非组件方式)修改状态也会重新渲染任何订阅的组件。

测试代码:

// Profile.js

import { useGlobalState } from "../store.config";

function Profile({
  const [user] = useGlobalState("user");

  return (
    <div className="Profile">
      <h3>{user.name}</h3>
      <p>{user.age}</p>
    </div>

  );
}

// Profile.test.js

import { render, screen } from "@testing-library/react";
import Profile from "./Profile";
import { setGlobalState } from "../store.config";

it("should display global user data", () => {
  setGlobalState("user", {
    name"Chris Stone",
    age20,
  });

  render(<Banner />);

  expect(screen.queryByText("Chris Stone")).toBeInTheDocument();
  expect(screen.queryByText("20")).toBeInTheDocument();
});

还有一些库的测试(createGlobalState.test.tsx)可在这里找到。

这种基本全局存储实现的思想并不新鲜。它受到类似项目(如react-hook-global-state)的启发,但没有引入外部依赖。

尽管简单,createGlobalState也是可扩展的。开发人员在流行的全局状态解决方案中所欣赏的功能,例如中间件、错误处理、actions等,都可以在其之上构建。

最后

还没有使用过我们刷题网站(https://fe.ecool.fun/)或者前端面试题宝典的同学,如果近期准备或者正在找工作,千万不要错过,题库主打无广告和更新快哦~。