如果你曾在一个应用程序上工作,其中有两个以上的不同祖先的组件必须共享相同的状态,你就会明白,将道具传递给所有这些组件会很快变得混乱。状态管理是在我们的应用程序中管理这些数据的一种方式,从一个文本字段的值到表格上的行。
进入状态管理库,如Redux。这些库的目的是解决这个问题,但它们仍然不完美。事实是,完美的状态管理库并不存在。在选择时有太多不同的因素需要考虑,比如你的应用程序的大小,你想实现的目标,以及有多少状态被共享。
在这篇文章中,我们将看看一些状态管理选项,以帮助你决定在你的React Native应用中使用哪一个。我将比较React Context API、Hookstate和Easy-Beasy的状态管理的开发者体验。
关于Redux等流行的状态管理的文章已经很多了,所以我将讨论这些小的状态管理,以帮助你做出一个明智的决定。
前提条件
为了跟上这篇文章,你应该具备以下条件。
- 对React和React Native的工作知识
- 在你的机器上安装了Node.js、npm或Yarn(npm是与Node.js打包的)。
- 在你的机器上安装XCode或Android Studio
- React Native文档中列出的其他必要的依赖条件
我将在本文中使用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的优点
使用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.