对比React Native状态管理库

1,911 阅读4分钟

如果你曾在一个应用程序上工作,其中有两个以上的不同祖先的组件必须共享相同的状态,你就会明白,将道具传递给所有这些组件会很快变得混乱。状态管理是在我们的应用程序中管理这些数据的一种方式,从一个文本字段的值到表格上的行。

进入状态管理库,如Redux。这些库的目的是解决这个问题,但它们仍然不完美。事实是,完美的状态管理库并不存在。在选择时有太多不同的因素需要考虑,比如你的应用程序的大小,你想实现的目标,以及有多少状态被共享。

在这篇文章中,我们将看看一些状态管理选项,以帮助你决定在你的React Native应用中使用哪一个。我将比较React Context API、Hookstate和Easy-Beasy的状态管理的开发者体验。

关于Redux等流行的状态管理的文章已经很多了,所以我将讨论这些小的状态管理,以帮助你做出一个明智的决定。

前提条件

为了跟上这篇文章,你应该具备以下条件。

我将在本文中使用Yarn,但如果你喜欢npm,请确保将命令替换为npm的等价物。

设置一个演示应用程序

由于这篇文章的性质,我们不会从头开始建立一个新的应用程序。因为我只讨论这些库的比较,所以我设置了一个演示应用程序,你可以跟随我展示它们的优势和劣势。

克隆 repo

你可以在这个Github repo找到我的演示应用程序。如果你在本地克隆它并安装必要的依赖项,你会看到已经为我们将要讨论的每个库的例子创建了分支。

git clone https://github.com/edmund1645-demos/comparing-rn-state-lib

安装依赖项

克隆该 repo 到你的本地机器后,使用你喜欢的任何软件包管理器安装依赖项。

npm install 
#or
yarn install

运行应用程序

你可以看看main 分支,特别是App.js 文件,以便在我们实现状态管理之前了解该应用程序的结构。

使用这个命令运行应用程序。

yarn ios #or npm 
#or
yarn android

用React Context API管理状态

我们将首先看一下ContextAPI。现在,我知道你在想什么:Context API不是一个 "独立的 "库。虽然这是事实,但它仍然是一个值得考虑的选择。

在克隆了 repo 并安装了依赖项之后,请查看context-example 分支。

git checkout context-example

现在看一下contexts/CartContext.js 文件。

import React, { createContext } from 'react';
export const initialState = {
  size: 0,
  products: {},
};
export const CartContext = createContext(initialState);

我们使用React的createContext 方法来创建一个上下文对象并将其导出。我们还传入一个默认值。

App.js ,我们首先导入CartContext 对象和默认值initialState

导入后,我们需要设置一个useReducer 钩子,根据动作类型修改状态。

//  import context
import { CartContext, initialState } from './contexts/CartContext.js';

// reducer function
const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TO_CART':
      if (!state.products[`item-${action.payload.id}`]) {
        return { size: (state.size += 1), products: { ...state.products, [`item-${action.payload.id}`]: { ...action.payload, quantity: 1 } } };
      } else {
        let productsCopy = { ...state.products };
        productsCopy[`item-${action.payload.id}`].quantity += 1;
        return { size: (state.size += 1), products: productsCopy };
      }
  }
};

export default function App() {
  // set up reducer hook
  const [state, dispatch] = useReducer(reducer, initialState);

  // create a Provider and use the return values from the reducer hook as the Provider's value
  return (
    <CartContext.Provider value={[state, dispatch]}>
      {/* other components */}
    </CartContext.Provider>
  )
}

当设置一个需要修改值的上下文时,就像我们的例子一样,我们需要使用一个reducer钩子。你会注意到我们在上面的代码中,在Provider中使用这个reducer钩子的值。这是因为reducer 函数更新了状态,所以我们想让状态(以及修改状态的函数)在我们所有的组件中都可用。

稍后,你会看到为什么子组件可以访问提供者的值,而不是创建上下文时的默认值。

接下来,看一下components/ProductCard.jsx 文件。

import React, {useContext} from 'react'
import {CartContext} from '../contexts/CartContext'

const ProductCard = ({ product }) => {
  const [state, dispatch] = useContext(CartContext)

  function addToCart() {
    dispatch({type: 'ADD_TO_CART', payload: product})
  }
  return (
      {/* children */}
   )
}

为了访问创建购物车上下文时的值,我们需要导入它并将其传递给useContext 钩。

请注意,返回的值是我们先前传递给提供者的数组,而不是创建上下文时的默认值。这是因为它使用了树上匹配的提供者的值;如果树上没有CartContext.Provider ,返回值将是initialState

当点击购物车按钮时,addToCart 被调用,一个动作被派发到我们的reducer函数以更新状态。如果你再看一下reducer 函数,你会发现有一个对象被返回;这个对象就是新的状态。

每次我们派发一个动作,都会返回一个新的状态,只是为了更新这个大对象的一个属性。

让我们看一下购物车的屏幕(screens/Cart.jsx)。

import React, {useContext} from 'react'
import { CartContext } from '../contexts/CartContext'

const Cart = () => {
  const [state, dispatch] = useContext(CartContext)
  return (
    {/* children */
  )
}

这里我们使用了与ProductCard.jsx 相同的模式,只是这次我们只使用了state 来渲染购物车项目。

使用Context API的优点和useReducer

  • 是小型项目的理想选择
  • 不影响包的大小

使用Context API的缺点useReducer

  • 更新大型对象会很快变得混乱
  • 可能不适合大型项目,因为如果需要的话,你需要在树上堆放多个Providers。

用Hookstate管理状态

Hookstate提供了一种不同的状态管理方法。它对于小型应用来说足够简单,对于相对大型的应用来说足够灵活。

请看hookstate-example 分支。

git checkout hookstate-example

通过Hookstate,我们使用了state/Cart.js全局状态的概念。该库导出了两个函数:createState ,通过在默认状态周围包裹一些属性和方法来创建一个新的状态,并将其返回;useState ,使用从createState 或其他useState 返回的状态。

import { createState, useState } from '@hookstate/core';

const cartState = createState({
  size: 0,
  products: {},
});

export const useGlobalState = () => {
  const cart = useState(cartState);
  return {
    get: () => cart.value,
    addToCart: (product) => {
      if (cart.products[`item-${product.id}`].value) {
        cart.products[`item-${product.id}`].merge({ quantity: cart.products[`item-${product.id}`].quantity.value + 1 });
        cart.size.set(cart.size.value + 1);
      } else {
        cart.products.merge({ [`item-${product.id}`]: { ...product, quantity: 1 } });
        cart.size.set(cart.size.value + 1);
      }
    },
  };
};

以Hookstate的结构方式,我们还可以导出一个辅助函数,用于与状态内的组件进行交互。

我们所需要做的就是导入useGlobalState ,在一个功能组件中调用它,并从返回的对象中解构任何方法(取决于我们想要实现的目标)。

下面是一个例子,说明我们如何在components/ProductCard.jsx 中使用addToCart 方法。

import { useGlobalState } from '../state/Cart';
const ProductCard = ({ product }) => {
  // invoke the function to return the object
  const state = useGlobalState()

  function addToCart() {
  // pass the product and let the helper function deal with the rest
    state.addToCart(product)
  }

  return (
    {/* products */}
  )
}

而在Cart 页面上,/screens/Cart.js

import { useGlobalState } from '../state/Cart';
const Cart = () => {
  const {products} = useGlobalState().get()
  return (
    {/* render every item from the cart here */}
  )
}

Hookstate最好的地方在于,全局状态中的每个属性或方法(包括嵌套的和顶层的)都是一种状态,并且有各种方法可以直接修改自己。它具有足够的反应性,可以更新整个应用程序中所有组件的状态。

使用Hookstate的优点

  • 简单的API就能完成工作
  • 执行力强
  • 大量的扩展来创建更多功能的应用程序
  • 完全类型化的系统

使用Hookstate的缺点

我知道我说过没有任何 "完美 "的替代品,但似乎Hookstate正试图推翻我的理论。然而,有一个可以忽略不计的因素需要考虑。Hookstate不是很出名--它在npm上的每周下载量大约为3000次,所以围绕它的社区有可能很小。

用Easy-Peasy管理状态

Easy-Peasy是Redux的一个抽象,它的建立是为了暴露一个简单的API,在保留Redux所提供的所有好处的同时,大大改善了开发者的体验。

我注意到,使用Easy-Peasy就像使用上面两个例子的组合,因为你必须把整个应用包裹在一个提供者周围(别担心,你只需要做一次,除非你想要模块化状态)。

要导入Easy-Peasy,请将以下内容复制到App.js

import { StoreProvider } from 'easy-peasy';
import cartStore from './state/cart';

export default function App() {

  return (
     <>
      <StoreProvider store={cartStore}>
        {/* children */}
      </StoreProvider>
    </>
  );

你可以从库中导入钩子,挑出你在组件中需要的全局状态的特定部分。

让我们看一下/state/Cart.js

import { createStore, action } from 'easy-peasy';
export default createStore({
  size: 0,
  products: {},
  addProductToCart: action((state, payload) => {
    if (state.products[`item-${payload.id}`]) {
      state.products[`item-${payload.id}`].quantity += 1;
      state.size += 1;
    } else {
      state.products[`item-${payload.id}`] = { ...payload, quantity: 1 };
      state.size += 1;
    }
  }),
});

我们使用createStore 来启动一个全局存储。所传递的对象被称为 "模型"。当定义模型时,我们也可以包括像行动这样的属性。行动允许我们更新商店中的状态。

components/ProductCard.jsx ,我们想使用addProductToCart 动作,所以我们利用了Easy-Peasy的useStoreActions 钩子。

import React from 'react';
import { useStoreActions } from 'easy-peasy';

const ProductCard = ({ product }) => {
  const addProductToCart = useStoreActions((actions)=> actions.addProductToCart)

  function addToCart() {
    addProductToCart(product)
  }
  return (
    {/* children */}
  )
}

如果我们想在一个组件中使用状态,我们使用useStoreState 钩子,就像在screens/Cart.jsx

import React from 'react';
import { useStoreState } from 'easy-peasy';

const Cart = () => {
  const products = useStoreState((state)=> state.products)
  return (
    {/* children */}
  )
}

使用Easy-Peasy的优点

  • 完全反应式
  • 建立在Redux上,所以支持Redux开发工具和更多。
  • 简单的API

使用Easy-Peasy的缺点

  • 增加了包的大小。如果这对你来说是个大问题,Easy-Peasy可能不是你的理想库。

总结

在这篇文章中,我们看了带钩子的Context API、Hookstate和Easy-Beasy之间的比较。

总结一下,在演示项目中使用带钩子的Context API是很理想的,但是当你的应用程序开始增长时,它就变得难以维护。这就是Hookstate和Easy-Peasy的闪光之处。

Hookstate和Easy-Peasy都提供了简单的API来管理状态,并且以独特的方式执行。Easy-Peasy是建立在Redux之上的,所以你有这些额外的好处,而Hookstate有一套扩展,可以在你的应用程序中实现一些功能,比如状态的本地存储持久化。

由于篇幅问题,本文没有提到许多替代方案,所以这里有一些值得推荐的方案。

你可以在这里找到这个项目的资源库,如果你想检查每个例子的代码。

The postComparing React Native state management librariesappeared first onLogRocket Blog.