6种方法来简化 React 中的状态管理(不使用全局状态)

968 阅读10分钟

image.png 本文为译文,原文地址:oskari.io/blog/stop-r…

不要什么都使用全局状态。在工作中使用合适的工具来完成我们的工作。

React是一个了不起的框架,但远非完美,它的不足之处之一就是状态管理

当React在2015年掀起热潮时,它所承诺的高性能虚拟DOM、反应式状态、单向数据绑定以及在传统项目中的迭代采用都令人难以置信。

然而,当我在大型应用程序中开始使用React,很快就遇到了挑战,如管理获取的数据,在整个应用程序中共享状态,在会话之间持久化和混合数据,以及用户会话管理。

2015年如果你想在任何大型应用中使用React,必须使用Redux。

如果使用不当,很容易引入一百万个模板文件,把你的代码库变成意大利面条,拖慢开发速度。尽管如此,Redux是一个神奇的、经过战斗考验的状态管理工具。但它只是:一个工具库。今天,我们有更多的工具可用--适用于不同的用例。

我想告诉你如何使用你已经掌握的技术来简化你的React状态。所以你不需要马上跳到这些工具中去。

这是两部分系列中的第一部分,我想告诉你在React应用程序中管理状态的更简单的方法。对于使用非生命周期工具,如本地浏览器存储,请看这里

不要这样!

你写React并不意味着每一个变量都应该生活在React状态中!我认为这是不对的。不幸的是,我看到很多新人犯了这个错。

有状态的数据会带来性能成本、可维护性,以及处理渲染生命周期的头痛问题。组件生命周期是React的面包和黄油,而状态是其核心。但是,如果你不需要对数据做出 "反应",就不要让它处于状态中。

状态和类型

image.png 在React应用程序中,有六种存储和访问数据的机制,我们将重点讨论。

  • 本地状态:只有一个组件需要该状态
  • Hooks:可重复使用的状态模式
  • 提升的状态:一些相关的组件需要该状态
  • 上下文:共享的解除状态的状态
  • 全局状态:第三方库
  • 请求的数据

1. 本地状态

本地状态是最简单的,也是最容易维护的(在一定程度上)状态管理。如果你的组件只需要在内部管理状态,那就把它放在那里吧! 让你的状态尽可能的接近它的用途。

在React的新版本中,本地状态是最简单的,因为使用了useState和useEffect hooks。

注意:使用useReducer等钩子可以有更高级的用例,但要警惕,因为这样的用例很快就会开始看起来像Redux的重写版

技巧:

  • 尽可能地保持你的状态是扁平的。
  • 嵌套对象不容易管理,而且会带来重新渲染的错误。
  • 不是所有的东西都需要成为一个状态对象。
  • 使用简洁和描述性的名字。
const Component = ({ onSelect }) => {
  const [data, setData] = useState([]);
  const [error, setError] = useState(null);
  const [selected, setSelected] = useState(null)
  useEffect(() => {
    // Fetch data to render on initial render of the component
    fetchData()
      .then(res => setData(res.data))
      .catch(e => setError(e));
  },[])
  useEffect(() => {
    // Do something when a user selects a row
    onSelect(selected)
  },[selected])
  return (
    <>
    {error && <Error message="Oops something went wrong! Failed to fetch data" />}
    <Table>
      <Header columns={["Name", "Description","Select"]}>
      {data.map(item => (
          <Row key={item.id} selected={selected === item.id}>
            <Cell>{item.name}</Cell>
            <Cell>{item.description}</Cell>
            <Cell><Button onClick={() => setSelected(item.id)} label="Select" /></Cell>
          </Row>
      ))}
    </Table>
    </>
  )
}

然而,我们不能创建只有本地状态的React应用程序。那样的话,用户体验就会很差。我们的组件需要传递和分享数据。这就是为什么状态管理是React架构的一个关键部分。

2. Hooks 呢?

自定义 React Hooks 本身并不是状态管理的一种形式,而是访问共享状态的优秀工具,如 Context、URL 和 Browser 存储。你也可以用它们将本地或提升的状态抽象成可重用的状态模式,在整个组件中使用,而无需在它们之间共享数据。

技巧:

  • 让你的 hooks 专注于特定的用例。不要试图把所有东西都塞进一个 hooks 里。把它按用例分离成多个 hooks。
  • 如果它是可重复使用的:记录钩子并将其放在其他开发者可以访问的目录中。我喜欢使用 @hooks 的别名,把素有全局可重用的 hooks 放在一个地方。 一个可重复使用的表单状态钩子的例子。
const useFormState = ({ initForm, validateFn }) => {
  const [formData, setFormData] = useState(initForm);
  const [dirty, setDirty] = useState(false);
  const [valid, setValid] = useState(false);
  const [validated, setValidated] = useState(false);
  const validate = () => {
    if (typeof validateFn === "function") {
      const isValid = validateFn(formData);
      setValid(isValid);
      setValidated(true);
    }
  };
  const onChange = ({ key, data }) => {
    setDirty(true);
    setValidated(false);
    setValid(false);
    setFormData((prevData) => ({ ...prevData, [key]: data }));
  };
  return { dirty, valid, validated, validate, onChange };
};

3. 提起的状态

image.png React的强大之处在于能够从许多组件中构建可组合的应用程序。为了协调这些组件并创造流畅的用户体验,它们往往需要相互传递或共享数据。为了做到这一点,我们可以将React树中的状态 "提升 "到一个共享的父组件中的本地状态。

你还是应该尽可能地让状态靠近树中需要它的组件。实现这种模式的最简单方法是将本地状态存储在父组件中,并将道具传递给子组件。

技巧:

  • 尽量保持道具的扁平化,以避免不必要的重新提交。
  • 尽可能地将解除的状态保持在需要它的组件中。
const Checkout = () => {
  const [items, setItems] = useState([]);
  const addItem = (item) => {
    setItems((items) => [...items, item]);
  };
  const removeItem = (id) => {
    setItems((items) => items.filter((item) => item.id !== id));
  };
  return (
    <>
      <ProductsList onSelect={addItem} />
      <ShoppingCart items={items} onRemove={removeItem} />
    </>
  );
};

然而,正如你们中的许多人所知道的,当你的父子树长得太大时,这种模式很快就会失效,你开始通过组件传递 prop 来获得低层节点。这被称为 "prop drilling(prop 下钻)",当它发生时,你需要考虑分解你的用例并引入其他状态模式,如React Context。

4. React Context

image.png

当你的状态需要被整个 React 树的组件访问时,最好将你的状态管理从树本身分离出来,以避免 prop drilling 。一个很好的方法是使用 React 的 Context API。你可以建立你自己的 "mini"全局状态来满足你的特定需求。

技巧:

  • 不要试图把所有的状态都塞进一个单一的上下文中。如果你这样做,你就是在重新创建一个全局状态管理工具,有很多第三方工具可以为你解决这个问题。
  • 保持你的上下文与用例相一致。专注于对状态数据的需求并只解决这个问题。
  • 尽量保持上下文的简单。 如果你开始注意到嵌套的Providers相互包裹的混乱局面,可以考虑使用Providers组件模式
  • 创建一个钩子来简化useContext(MyContext)的使用,使其只需使用MyContext。
// Context
const CartContext = React.createContext();

const useCart = useContext(CartContext);

const CartProvider = ({ children }) => {
  const [items, setItems] = useState([]);
  const addItem = (item) => {
    setItems((items) => [...items, item]);
  };
  const removeItem = (id) => {
    setItems((items) => items.filter((item) => item.id !== id));
  };
  return (
    <CartContext.Provider value={{ items, addItem, removeItem }}>
      {children}
    </CartContext.Provider>
  );
};

// App
<CartContext.Provider>
  <App />
</CartContext.Provider>;

// Component
const ShoppinCart = () => {
  const { items, removeItem } = useCart();
  return (
    <>
      {items.map((item) => (
        <Item {...item} key={item.id} remove={removeItem} />
      ))}
    </>
  );
};

5. 全局状态库

众多与 Redux 不同的状态管理库出现,让人感到欣喜。Redux 对状态管理采取了自上而下的观点,迫使你从状态树的顶端开始,然后一路向下。许多新的库引导你从下往上开始。首先从消耗的组件和它们的用例开始。这更有利于消费组件的模块化和可组合性。

这也正是我们所要做的,将状态尽可能地保持在消费组件附近--将状态 "pushing"到组件树下。

当你确实发现自己处于这样一种情况:React Context 和其他方法不再满足并开始引入调试问题,或者你在用 useReducer 重写 Redux 时,看看这些新的流行的库。

  • Zustand:小型、快速、可扩展的全局状态工具,遵循 Flux 原则。
  • Recoil:全局 React 状态在原子图结构中与你的 React 树平行。
  • Jotai: 原子状态管理,灵感来自 Recoil。
  • Xstate:框架无关的工具,用于构造状态机中的状态和 UI 交互。

6. 获取的数据应该放在哪里?

通过API从数据库中获取的数据应该遵循与所有其他类型的数据相同的状态规则。但它确实带来了一些我们应该考虑的新模式。

如果获取的数据很大,就有机会进行缓存--但要注意!缓存似乎是一种优化,但它并不适合我们。缓存似乎是一种优化,但如果你不小心的话,可能会带来许多具有挑战性的bug需要调试。你应该尽可能地在靠近源头的地方缓存数据。首先选择的是在浏览器外缓存数据,以获得更多的可扩展的解决方案。

如果你需要在浏览器中进行,请保持与你用来获取数据的机制接近。不管是 axios、superagent、fetch,还是你自己对这些的包装。我喜欢使用 React hooks 来控制 API 交互,这样你就可以轻松地跟踪数据、加载和错误状态。如果这听起来很有趣,请查看 react-query。

const { isLoading, error, data, isFetching } = useQuery("repoData", () =>
  axios
    .get("https://api.github.com/repos/tannerlinsley/react-query")
    .then((res) => res.data)
);

就像其他数据一样,如果需要被其他组件访问,只需将其在 "树 "上移到可重用的 hooks、Context 或全局状态。保持数据在树中最低的共享点,以避免污染整个应用程序。

额外的——衍生状态

衍生状态不是一种新的状态管理形式,而是另一种完全避免状态管理的提示。

如果你有一个现有的状态,并需要将其转换为不同的形式或使用一个子集,你不需要将结果存储在另一个状态对象中。真相的来源已经在状态中了,所以任何生命周期都会在你的组件上触发重新渲染。利用这个优势,只需从现有的状态值中 "派生 "你的新数据--无论是本地状态、道具,还是其他地方。

不要这样做。

const AdminUsers = ({ users }) => {
  const [adminUsers, setAdminUsers] = useState([]);
  useEffect(() => {
    setAdminUsers(
      users.filter((user) => user.permissions.indexOf("admin") > -1)
    );
  }, [users.length]);
  return (
    <div>
      {adminUsers.map((user) => (
        <span key={user.id}>{user.name}</span>
      ))}
    </div>
  );
};

用下面的替换:

const AdminUsers = ({ users }) => {
  // Derived state:
  const adminUsers = users.filter(
    (user) => user.permissions.indexOf("admin") > -1
  );
  return (
    <div>
      {adminUsers.map((user) => (
        <span key={user.id}>{user.name}</span>
      ))}
    </div>
  );
};

最后的思考

状态管理是一个棘手的问题,但对 React 架构的成功至关重要。始终对你的状态用例持批判态度,并仔细考虑要遵循哪种模式。尽量让你的状态接近于它的用途,避免不必要地分享别人不使用的状态而污染你的代码库。

而且不要停止! 所有的软件都是不断变化的,所以要寻找机会重构你的状态,以最好地满足应用程序的需求,并帮助保持你的代码库的可扩展性。没有什么比试图建立一个新的功能而不知道应该使用什么状态或如何访问它更糟糕了。

我希望这能帮助你更严谨地思考如何根据用例在React应用程序中最好地存储数据。

我很想听听你的意见。让我知道你在评论中的想法。你在状态方面遇到了什么类型的挑战?你最喜欢的状态模式是什么?

如果你喜欢这篇文章,请查看第二部分

如有感觉翻译不合适的地方,欢迎交流,目前处于学习翻译的路上。 公众号 三只快乐虫 期待你的关注!