如何在 React 应用中使用 Hooks、Redux 等管理状态

大家好,我是若川。持续组织了近一年的源码共读活动,感兴趣的可以 点此扫码加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。另外:目前建有江西|湖南|湖北籍前端群,可加我微信进群。


在本文中,我们将了解在 React 应用程序中管理状态的多种方式。

我们将从讨论什么是状态开始,然后介绍许多用于管理状态的工具。

我们将了解简单的 useState hook,并学习更复杂的库,如 Redux。然后我们将查看最新可用的库,例如 Recoil 和 Zusand。

目录

  • React 中的状态是什么

  • 如何使用 useState hook

    • 如何使用 useEffect 读取状态更新

    • 如何传递一个回调给状态更新函数

  • 管理规模和复杂性

    • React context

    • 如何使用 useReducer hook

    • 那么 Redux 呢

  • Redux 的替代品

    • 提到 Redux Thunk 和 Redux Saga

    • Redux toolkit

    • Recoil

    • Jotai

    • Zustand

  • 总结

React 中的状态是什么

在现代 React 中,我们使用函数组件构建我们的应用程序。组件本身就是 JavaScript 函数,是独立且可复用的代码。

使用组件构建应用程序的目的是使其具有模块化架构,具有明确的关注点分离。这使代码更易于理解、更易于维护并且在可能的情况下更易于复用。

而状态(state)是一个保存有组件信息的对象。普通 JavaScript 函数没有存储信息的能力。一旦执行完成,它们中的代码就会执行并“消失”。

但是有了状态之后,React 函数组件即使在执行后也可以存储信息。当我们需要一个组件来存储或“记住”某些东西,或者根据环境以不同的方式执行时,状态就是我们所需要的可以让这些生效的东西。

值得一提的是,在 React 应用程序中的并非所有组件都必须具有状态,也有无状态组件,它们只呈现其内容而无需存储任何信息,这也很好。

另一件重要的事情是状态变化是使 React 组件重新渲染的两个原因之一(另一个是 props 的变化)。因此,状态存储了组件的信息同时也控制了它的行为。

如何使用 useState hook

为了在我们的组件中实现状态,React 为我们提供了一个名为 useState 的钩子(hook)。让我们看看它是如何与以下示例一起工作的。

我们将使用经典的计数器示例,其中我们将显示一个数字,并且我们有几个按钮用于增加、减少或重置该数字。

这是一个很好的应用程序示例,我们需要存储一条信息并在每次信息更改时呈现不同的内容。

2821faa00936bdd8aaebfb2b659e97ad.gif

此应用程序的代码如下所示:

// App.js
import { useState } from 'react'function App() {const [count, setCount] = useState(0)return (<div className="App"><p>Count is: {count}</p><div><button onClick={() => setCount(count+1)}>Add 1</button><button onClick={() => setCount(count-1)}>Decrease 1</button><button onClick={() => setCount(count+10)}>Add 10</button><button onClick={() => setCount(count-10)}>Decrease 10</button><button onClick={() => setCount(0)}>Reset count</button></div></div>)
}export default App
  • 首先我们从 React 导入钩子(hook):import { useState } from 'react'

  • 然后我们初始化状态:const [count, setCount] = useState(0)

在这里,我们为状态提供了一个变量名(count)和一个我们将在每次需要更新该状态时使用的函数名(setCount)。最后,我们设置状态的初始值(0),这将是应用程序每次启动时默认加载的值。

  • 最后,如上所述,每次我们想要更新状态时,都必须使用我们声明的函数:setCount,只需要调用它并将我们想要的新状态作为参数传递给它。也就是说,如果我们想在前一个状态加 1,我们需要调用 setCount(count+1)

如前所述,这将导致状态更新,从而导致组件的重新渲染。在我们的应用程序中我们将在屏幕上看到计数器增加。

如何使用 useEffect 读取状态更新

一个需要提到的重要信息是 setState 函数是异步的。因此,如果我们尝试在更新状态后立即读取它,例如:

<button onClick={() => {setCount(count+1)console.log(count)
}}>Add 1</button>

我们会得到之前的状态值,并没有得到更新。

在更新状态后读取状态的正确方法是使用 useEffect hook。它允许我们在每个组件重新渲染后(默认情况下)或在我们声明更改的任何特定变量之后执行一个函数。

就像这样:

useEffect(() => console.log(value), [value])

如何传递一个回调给状态更新函数

在非常频繁和快速的状态变更时,异步的 useState 也会产生一些影响。

举个例子,用户连续多次按下 ADD 按钮,或者一个循环中发出一定次数的点击事件。

通过setCount(count+1)更新状态,在下一个事件被触发时 count 会有不被更新的风险。

例如,假设在开始时 count = 0,然后调用 setCount(count+1) 异步更新状态。

但是在状态更新完成之前再次调用了 setCount(count+1)。这意味着仍然是count = 0,这意味着第二个setCount不会正确更新状态。

一种比较防守型的方法是向 setCount 传递一个回调,如下所示:setCount(prevCount => prevCount+1)

这样可以确保要更新的值是最新的,并使我们远离上述问题。每次我们对先前的状态执行更新时,我们都应该使用这种方法。

管理规模和复杂性

到目前为止,状态管理似乎是小菜一碟。我们只需要一个 hook、一个值和一个函数来更新它,我们就可以开始了。

但是,一旦应用程序开始变得更大更复杂时,仅使用这一种方式可能会开始导致一些问题。

React context

第一个可能出现的问题是当我们有很多嵌套组件时,我们需要许多“兄弟”组件来共享相同的状态。

显而易见的答案是“提升”状态,这意味着父组件将成为持有状态的组件,并将状态作为 prop 传递给子组件。

这很好用,但是当我们有很多嵌套组件时,可能需要通过许多层级组件传递 props。这被称为 prop drilling,不仅很难看,而且会创建难以维护的代码。

Prop drilling 还可能导致不必要的重新渲染,这可能会影响我们应用程序的性能。如果在我们的父组件(存储状态)和子组件(使用状态)之间还有其他组件(“中间组件”),我们也需要通过这些中间组件传递 prop,即使它们并不需要 prop。

这意味着这些“中间组件”将在 prop 变更时重新渲染,即使它们没有不同的内容需要渲染。

解决这个问题的一种方法是使用 React context,简单来说,这是一种创建包装组件的方法,该组件包装我们那些想要并且可以直接传递 props 的组件组,而且无需 “drill” 通过那些不是必须使用该状态的组件。

使用 context 时要注意的是,当 context 状态发生变化时,所有接收该状态的被包装组件都将重新渲染。这种情况下,这可能不是必然的,也可能导致性能问题。

因此,我们是否真的需要让一个状态对许多组件可用,或者我们是否可以将其保持在单个组件中, 在这两者之前取一个平衡是非常重要的。如果我们需要让许多组件都可以使用它,把它放在 context 中真的是一个好主意吗?或者我们是否可以把它提升一个层次?

Kent C Dodds 有一篇关于这个主题的很酷的文章。

如何使用 useReducer hook

当你使用 useState 时,要设置的新状态取决于先前的状态(如我们的计数示例),或者当我们的应用程序中状态更改非常频繁,这种情况下可能会出现另一个问题。

在这些情况下,useState 可能会引发一些意想不到的和不可预知的行为。接下来的 reducers 将解决这个问题。

reducer 是一个纯函数,它将前一个状态和一个动作作为参数,并返回下一个状态。它被称为 reducer,是因为它与你传递给数组的函数类型相同:Array.prototype.reduce(reducer, initialValue)

useReducer 是 React 提供的 hook,它让我们实现 reducer 来管理状态。使用这个 hook,我们之前的示例应用程序看起来像这样:

// App.js
import { useReducer } from 'react'
import './App.scss'function App() {function reducer(state, action) {switch (action.type) {case 'ADD': return { count: state.count + 1 }case 'SUB': return { count: state.count - 1 }case 'ADD10': return { count: state.count + 10 }case 'SUB10': return { count: state.count - 10 }case 'RESET': return { count: 0 }default: return state}}const [state, dispatch] = useReducer(reducer, { count: 0 })  return (<div className="App"><p>Count is: {state.count}</p><div><button onClick={() => dispatch({type: 'ADD'})}>Add 1</button><button onClick={() => dispatch({type: 'SUB'})}>Decrease 1</button><button onClick={() => dispatch({type: 'ADD10'})}>Add 10</button><button onClick={() => dispatch({type: 'SUB10'})}>Decrease 10</button><button onClick={() => dispatch({type: 'RESET'})}>Reset count</button></div></div>)
}export default App
  • 我们再次从 React 导入 hook 开始:import { useReducer } from 'react'

  • 然后我们将声明一个 reducer 函数,将接收当前的状态和对其执行的动作(action)作为参数。并且在函数里有一个 switch 语句,该语句将读取动作类型,对状态执行相应的动作,并返回更新后的状态。

通常做法是在 reducer 上使用 switch 语句, 并且使用大写字母来声明动作。

function reducer(state, action) {switch (action.type) {case 'ADD': return { count: state.count + 1 }case 'SUB': return { count: state.count - 1 }case 'ADD10': return { count: state.count + 10 }case 'SUB10': return { count: state.count - 10 }case 'RESET': return { count: 0 }default: return state}}
  • 之后,是时候声明我们的 useReducer hook 了,它看起来与 useState hook 非常相似。我们为我们的状态声明一个变量(在我们的例子中是'state'),和一个我们将用来修改这个变量的函数('dispatch'),然后 useReducer 将接收上面的 reducer 函数 作为第一个参数,和一个默认状态作为第二个参数。

const [state, dispatch] = useReducer(reducer, { count: 0 })
  • 最后,我们不会直接调用 reducer 去更新状态,而是调用我们刚刚创建的函数('dispatch'),将我们想要执行的对应的动作类型传递给它。其中,dispatch 函数将与 reducer 连接并实际修改 state。

<button onClick={() => dispatch({type: 'ADD'})}>Add 1</button>

这比使用 useState 多了不少模板,但useReducer毕竟没有那么复杂。

总结一下,我们只需要:

  • 一个 reducer,合并所有可能的状态变化的函数

  • 一个 dispatch 函数,将修改动作传递给 reducer

这里的问题是, UI 元素将不能像以前那样通过用一个值调用 setState 去直接更新状态。现在它们需要调用一个动作类型(action type)并通过 reducer,这使得状态管理更加模块化和可预测。

那么 Redux 呢

Redux 是一个已经存在很长时间并且在 React 中被广泛使用的库。

Redux 是一个工具,它可以解决前面提到的两个问题(prop drilling 和频繁和复杂状态变更时不可预测的状态行为)。

值得一提的是,Redux 是一个不可知的库,这意味着它可以在任何前端应用程序上实现,不仅仅是 React。

Redux 工具集与我们刚刚看到的 useReducer 非常相似,但多了一些东西。Redux 中有三个主要的构建块:

  • store — 一个保存应用状态数据的对象

  • reducer — 一个由动作类型(action type)触发,并返回一些状态数据的函数

  • action — 一个告诉 reducer 如何改变状态的对象,它必须包含一个 type 属性,并且它还可以包含一个可选的 payload 属性

实现 Redux,我们的示例应用程序如下所示:

// App.js
import './App.scss'import { Provider, useSelector, useDispatch } from 'react-redux'
import { addOne, subOne, addSome, subSome, reset } from './store/actions/count.actions'import store from './store'function App() {const dispatch = useDispatch()const count = useSelector(state => state.count)return (<Provider store={store}><div className="App"><p>Count is: {count}</p><div><button onClick={() => dispatch(addOne())}>Add 1</button><button onClick={() => dispatch(subOne())}>Decrease 1</button><button onClick={() => dispatch(addSome(10))}>Add 10</button><button onClick={() => dispatch(subSome(10))}>Decrease 10</button><button onClick={() => dispatch(reset())}>Reset count</button></div></div></Provider>)
}export default App

此外,我们需要一个新的 store 文件夹,包含相应的 store、reducer 和 actions 文件。

// index.js (STORE)
import { createStore } from 'redux'
import CountReducer from './reducers/count.reducer'export default createStore(CountReducer)
// count.reducer.js
import { ADD, SUB, ADDSOME, SUBSOME, RESET } from '../actions/count.actions'const CountReducer = (state = { count: 0 }, action) => {switch (action.type) {case ADD: return { count: state.count + 1 }case SUB: return { count: state.count - 1 }case ADDSOME: return { count: state.count + action.payload }case SUBSOME: return { count: state.count - action.payload }case RESET: return { count: 0 }default: return state}
}export default CountReducer
// count.actions.js
export const ADD = 'ADD'
export const addOne = () => ({ type: ADD })export const SUB = 'SUB'
export const subOne = () => ({ type: SUB })export const ADDSOME = 'ADDSOME'
export const addSome = (value) => ({type: ADDSOME,payload: value
})export const SUBSOME = 'SUBSOME'
export const subSome = (value) => ({type: SUBSOME,payload: value
})export const RESET = 'RESET'
export const reset = () => ({ type: RESET })

这比我们之前看到的还要更多的模板(这也是 Redux 被批评的主要原因),所以让我们把它分解成几块:

  • 正如我提到的,Redux 是一个外部库,所以在进行任何操作之前,我们需要通过运行 npm i redux react-redux 来安装它。redux 将带来管理状态所需的核心函数,而react-redux 将安装一些很酷的 hook,可以轻松地从我们的组件中读取和修改状态。

  • 现在,首先是 store。在 Redux 中,store 是拥有所有应用程序状态信息的实体。多亏 Redux,我们能够从任何想要的组件中访问 store(就像使用 context 一样)。

为了创建一个 store,我们导入 createStore 函数,并将一个 reducer 函数作为输入传递给它。

要知道,你也可以将不同的 reducers 合并然后传递给同一个 store,这样你就可以将关注点分离到不同的 reducers 中。

import { createStore } from 'redux'
import CountReducer from './reducers/count.reducer'export default createStore(CountReducer)
  • 然后是 reducer,它的工作方式与之前我们看到的 useReducer 完全相同。它接收默认状态和一个动作(action)作为参数,然后在它里面有一个 switch 语句来读取 action type,执行相应的状态修改,并返回更新后的状态。

import { ADD, SUB, ADDSOME, SUBSOME, RESET } from '../actions/count.actions'const CountReducer = (state = { count: 0 }, action) => {switch (action.type) {case ADD: return { count: state.count + 1 }case SUB: return { count: state.count - 1 }case ADDSOME: return { count: state.count + action.payload }case SUBSOME: return { count: state.count - action.payload }case RESET: return { count: 0 }default: return state}
}export default CountReducer
  • 接着是 actions。actions 用于告诉 reducer 如何更新状态。在代码中,你可以看到,对于每个 action,我们都声明了常量来代替普通的字符串(这是一个可以提高可维护性的好做法),以及一些仅返回一个 type 或者 一个 type 和一个 payload 的函数。这些函数就是我们要从组件中 dispatch 去更改状态的函数。

请注意,我对这个例子做了一些改变,以显示在谈论 actions 时 payload 的含义。如果我们想在 dispatch action 时从组件传递一个参数payload 就是存放该信息的地方

在示例中,你可以看到我们在调用 ADDSOME/SUBSOME 时可以直接从组件中传递我们想要加/减的数字。

export const ADD = 'ADD'
export const addOne = () => ({ type: ADD })export const SUB = 'SUB'
export const subOne = () => ({ type: SUB })export const ADDSOME = 'ADDSOME'
export const addSome = value => ({type: ADDSOME,payload: value
})export const SUBSOME = 'SUBSOME'
export const subSome = value => ({type: SUBSOME,payload: value
})export const RESET = 'RESET'
export const reset = () => ({ type: RESET })
  • 最后是我们的组件。这里有 3 件事需要注意:

  1. 首先,我们有一个 provider 组件,它接收 store 作为 props。这是对所有被包装在其中的组件访问 store 的授权。

  2. 然后我们有一个名为 **useDispatch()**(我们将用于 dispatch actions)和另一个名为 useSelector() 的 hook(我们将用于从 store 中读取状态)。

  3. 最后,请注意我们将要 dispatch 我们在 action 文件中声明的函数,并传递一个匹配的值作为输入。这个值是 actions 接收作为 payload 的值,以及 reducer 将用来修改状态的值。

import './App.scss'import { useSelector, useDispatch } from 'react-redux'
import { addOne, subOne, addSome, subSome, reset } from './store/actions/count.actions'function App() {const dispatch = useDispatch()const count = useSelector(state => state.count)return (<div className="App"><p>Count is: {count}</p><div><button onClick={() => dispatch(addOne())}>Add 1</button><button onClick={() => dispatch(subOne())}>Decrease 1</button><button onClick={() => dispatch(addSome(10))}>Add 10</button><button onClick={() => dispatch(subSome(10))}>Decrease 10</button><button onClick={() => dispatch(reset())}>Reset count</button></div></div>)
}export default App

Redux 是一个很好的工具,它同时解决了两个问题(prop drilling 和复杂的状态变化)。不过,它确实产生了很多模板,使状态管理成为一个更难理解的话题,特别是在处理不同的文件和实体,如 actions、reducers、store......

这里要提到的重要一点是,这些管理状态的工具或方法并不是相互排斥的,它们可以而且可能应该同时使用,并各自解决它们所擅长的具体问题。

对于 Redux,要解决的问题是处理全局状态(指影响整个应用程序或其中很大一部分的状态)。用 Redux 来处理像我们的例子中的计数器或模态的打开和关闭是没有意义的。

一个好的黄金法则是 useState 用于组件状态,Redux 用于应用程序状态

Redux 的替代品

如果这个主题对你来说还不够复杂,在过去的几年里,出现了许多作为 Redux 替代品的新的库,每个库都有自己的状态管理方法。

为了获得很好的概述,让我们快速了解它们。

Redux toolkit

Redux toolkit 是一个建立在 Redux 之上的库,其目的是去除 Redux 产生的一些复杂性和模板。

Redux toolkit 基于两件事:

  • store,它的工作方式与普通 Redux store 完全相同

  • slices 将普通的 Redux actions 和 reducer 压缩成一个单一的东西

实现 Redux Toolkit,我们的示例应用程序如下所示:

// App.js
import './App.scss'import { useSelector, useDispatch } from 'react-redux'
import { addOne, subOne, addSome, subSome, reset } from './store/slices/count.slice'function App() {const dispatch = useDispatch()const count = useSelector(state => state.counter.count)return (<div className="App"><p>Count is: {count}</p><div><button onClick={() => dispatch(addOne())}>Add 1</button><button onClick={() => dispatch(subOne())}>Decrease 1</button><button onClick={() => dispatch(addSome(10))}>Add 10</button><button onClick={() => dispatch(subSome(10))}>Decrease 10</button><button onClick={() => dispatch(reset())}>Reset count</button></div></div>)
}export default App
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { Provider } from 'react-redux'
import store from './store/index'ReactDOM.render(<React.StrictMode><Provider store={store}><App /></Provider></React.StrictMode>,document.getElementById('root')
)
// Index.jsx (STORE)
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './slices/count.slice'export const store = configureStore({reducer: {counter: counterReducer},
})export default store
// count.slice.jsx
import { createSlice } from '@reduxjs/toolkit'const initialState = { count: 0 }export const counterSlice = createSlice({name: 'counter',initialState,reducers: {addOne: state => {state.count += 1},subOne: state => {state.count -= 1},addSome: (state, action) => {state.count += action.payload},subSome: (state, action) => {state.count -= action.payload},reset: state => {state.count = 0}},
})export const { addOne, subOne, addSome, subSome, reset } = counterSlice.actionsexport default counterSlice.reducer
  • 首先我们需要通过运行 npm install @reduxjs/toolkit react-redux 来安装它

  • 在我们的 store 中,我们从 Redux toolkit 中导入 configureStore 函数,通过调用此函数来创建 store,并将一个带有 reducer 的对象传递给它,该对象本身就是一个包含 slice 的对象。

export const store = configureStore({reducer: {counter: counterReducer},
})
  • 正如我所提到的,slice 是一种将 action 和 reducer 压缩为同一个的方法。我们从 Redux toolkit 中导入createSlice 函数,然后声明初始状态并初始化 slice。

这个函数将接收 slice 的名称、初始状态以及我们将从组件派发以修改状态的函数作为参数。

注意这里没有任何 actions。UI 将直接调用 reducer 函数。这就是 Redux toolkit “带走”的复杂性。

export const counterSlice = createSlice({name: 'counter',initialState,reducers: {addOne: state => {state.count += 1},subOne: state => {state.count -= 1},addSome: (state, action) => {state.count += action.payload},subSome: (state, action) => {state.count -= action.payload},reset: state => {state.count = 0}},
})
  • 在 index.js 中,我们用 provider 组件包装应用程序,这样的话我们可以从任何地方访问状态。

<Provider store={store}><App /></Provider>
  • 最后,我们使用 hooks 从组件中读取状态和 dispatch 修改函数,就像使用普通的 Redux 一样。

function App() {const dispatch = useDispatch()const count = useSelector(state => state.counter.count)return (<div className="App"><p>Count is: {count}</p><div><button onClick={() => dispatch(addOne())}>Add 1</button><button onClick={() => dispatch(subOne())}>Decrease 1</button><button onClick={() => dispatch(addSome(10))}>Add 10</button><button onClick={() => dispatch(subSome(10))}>Decrease 10</button><button onClick={() => dispatch(reset())}>Reset count</button></div></div>)
}

Redux toolkit 旨在成为处理 Redux 的一种更简单的方法,但在我看来,它仍然是几乎相同的模板,与普通的 Redux 没有太大区别。

提到 Redux Thunk 和 Redux Saga

Redux thunk 和 Redux Saga 是两个与 Redux 一起使用的很流行的中间件库;

具体来说,Thunk 和 Saga 都是为了处理副作用或异步任务所使用的。

Recoil

32014e3d855db0112894a84d91b06cf4.png

Recoil 是一个开源状态管理库,专门用于由 Facebook(或 Meta,等等)构建的 React。根据他们的网站,Recoil是为“最小化和响应式”而建立的,在这个意义上,它看起来和感觉都像普通的 React 代码。

Recoil 基于原子(atom) 的概念。来自他们的文档,

“一个原子代表一个状态。原子可以从任何组件读取和写入。读取原子值的组件隐式订阅了该原子,因此任何原子更新都会导致所有订阅该原子的组件重新渲染”。

使用 Recoil,我们的示例应用程序将如下所示:

// App.js
import countState from './recoil/counter.atom'
import './App.scss'import { useRecoilState } from 'recoil'function App() {const [count, setCount] = useRecoilState(countState)return (<div className="App"><p>Count is: {count}</p><div><button onClick={() => setCount(count+1)}>Add 1</button><button onClick={() => setCount(count-1)}>Decrease 1</button><button onClick={() => setCount(count+10)}>Add 10</button><button onClick={() => setCount(count-10)}>Decrease 10</button><button onClick={() => setCount(0)}>Reset count</button></div></div>)
}export default App
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { RecoilRoot } from 'recoil'ReactDOM.render(<React.StrictMode><RecoilRoot><App /></RecoilRoot></React.StrictMode>,document.getElementById('root')
)
// counter.atom.jsx
import { atom } from 'recoil'const countState = atom({key: 'countState', // 唯一 ID (区别于其他 atoms/selectors)default: 0 // 默认值 (又称初始值)
})export default countState

你可能马上就能看到,这比 Redux 的模板要少很多。

  • 首先我们通过运行 npm install recoil 来安装它

  • 那些使用 recoil 状态的组件需要在其父组件的某处使用 RecoilRoot,所以我们用它来包装我们的应用程序

<React.StrictMode><RecoilRoot><App /></RecoilRoot></React.StrictMode>
  • 然后我们声明我们的 atom,它只是一个包含键和默认值的对象:

const countState = atom({key: 'countState', // 唯一 ID (区别于其他 atoms/selectors)default: 0 // 默认值 (又称初始值)
})
  • 最后,在我们的组件中,我们导入 useRecoilState hook,用它声明我们的状态,并将我们刚刚在 atom 中声明的唯一键传递给它

const [count, setCount] = useRecoilState(countState)

正如你所看到的,这看起来与常规的 useState hook 非常相似。

在我们的 UI 中,我们只是调用 setCount 函数来更新我们的状态。

<button onClick={() => setCount(count+1)}>Add 1</button>

最小,并且非常易于使用。Recoil 仍然是一种实验性的,并没有被广泛使用,但你可以看到世界各地的开发人员将如何转向这个工具。

Jotai

Jotai 是一个为 React 构建的开源状态管理库,其灵感来自 Recoil。它与 Recoil 的不同之处在于寻找一个更加简约的 API -- 它不使用字符串键,而且是面向 TypeScript 的。

与 Recoil 一样,Jotai 使用 atoms。atom 代表一片状态。你只需要指定一个初始值,它可以是原始值,如字符串和数字、对象和数组。然后在你的组件中使用该 atom,在每次 atom 更改时该组件将重新渲染。

使用 Jotai,我们的示例应用程序如下所示:

// App.js
import './App.scss'import { useAtom } from 'jotai'function App() {const [count, setCount] = useAtom(countAtom)return (<div className="App"><p>Count is: {count}</p><div><button onClick={() => setCount(count+1)}>Add 1</button><button onClick={() => setCount(count-1)}>Decrease 1</button><button onClick={() => setCount(count+10)}>Add 10</button><button onClick={() => setCount(count-10)}>Decrease 10</button><button onClick={() => setCount(0)}>Reset count</button></div></div>)
}export default App
// counter.atom.jsx
import { atom } from 'jotai'const countAtom = atom(0)export default countAtom

如你所见,它比 Recoil 还要小。

  • 我们通过运行 npm install jotai 来安装它

  • 然后我们声明一个具有默认值的 atom:

const countAtom = atom(0)
  • 在我们的组件中,通过使用 useAtom 去使用它:

const [count, setCount] = useAtom(countAtom)

真的很好,很简单!

Zustand

Zusand 是另一个为 React 构建的开源状态管理库。它的灵感来自于在 Redux 出现之前广泛使用的库 Flux,它的目标是

“一个小型的、快速的、非观点性的、可扩展的准系统状态管理解决方案,具有基于 hooks 的舒适 API,并且几乎没有模板”

Zusand 使用 store 的方式与 Redux 类似,但不同之处在于,在 Zusand 中,store 是一个 hook,它需要的模板要少得多。

使用 Zusand,我们的示例应用程序将如下所示:

// App.js
import './App.scss'
import useStore from './store'function App() {const count = useStore(state => state.count)const { addOne, subOne, add10, sub10, reset } = useStore(state => state)return (<div className="App"><p>Count is: {count}</p><div><button onClick={() => addOne()}>Add 1</button><button onClick={() => subOne()}>Decrease 1</button><button onClick={() => add10()}>Add 10</button><button onClick={() => sub10()}>Decrease 10</button><button onClick={() => reset()}>Reset count</button></div></div>)
}export default App
// Index.jsx (STORE)
import create from 'zustand'const useStore = create(set => ({count: 0,addOne: () => set(state => ({count: state.count += 1})),subOne: () => set(state => ({count: state.count -= 1})),add10: () => set(state => ({count: state.count += 10})),sub10: () => set(state => ({count: state.count -= 10})),reset: () => set({count: 0})
}))export default useStore
  • 我们通过运行 npm install zustand 安装它

  • 我们使用从 Zusand 导入的 create 函数创建了一个 store,在函数里我们设置了默认状态和我们将用来修改状态的函数

const useStore = create(set => ({count: 0,addOne: () => set(state => ({count: state.count += 1})),subOne: () => set(state => ({count: state.count -= 1})),add10: () => set(state => ({count: state.count += 10})),sub10: () => set(state => ({count: state.count -= 10})),reset: () => set({count: 0})
}))
  • 然后在我们的组件中导入刚刚创建的 store,并通过以下方式从中读取状态和修改函数:

const count = useStore(state => state.count)const { addOne, subOne, add10, sub10, reset } = useStore(state => state)

在我们的 UI 中可以像这样调用修改函数:

<button onClick={() => addOne()}>Add 1</button>

你可以看到来自 Redux 相同概念的 Zusand,却有一个更干净、更简单的方法。

总结

状态管理是前端开发中最复杂的主题之一。你可以看到有多少人试图让它以可预测和可扩展的方式,而且是以干净和易于使用的方式工作。

特别是在过去的几年里,出现了很多很好的工具,提供了处理状态管理的好方法。

不过,作为开发者,我们必须牢记,Redux 和其他库的创建是为了解决特定的状态管理问题,特别是在真正的大型、复杂和大量使用的应用程序中。

我认为,如果你没有遇到这些问题,真的没有必要增加额外的模板,使你的代码复杂化。即使使用那些几乎不添加样板的现代库。

React 本身是一个非常强大和可靠的库,useState、useReducer 和 useContext 等工具足以解决大多数问题。因此,我会坚持基本的东西,除非因为某些原因,基本的东西已经不够用了。

当需要更具体、更强大的状态管理库时,我认为应该在可靠性和简单性之间做出选择。

Redux 是最成熟和使用最广泛的库,它附带大量文档、在线社区以及在每个新版本中发现和解决的以前错误。

它的坏处是,作为开发者,它给我们带来了一些我们必须学习和思考的新概念。我们还需要添加相当多的代码来使其工作,而且它所增加的复杂性可能超过它所帮助解决的问题。

相反,我们之前所看到的现代库要简单得多,而且直截了当,但是它们没有得到广泛使用和测试,仍然是一种实验性的。

但就我们目前所看到的而言,其中一个或一些带头成为更广泛使用的工具似乎只是时间问题。

我希望你喜欢这篇文章并学到了一些新东西。

干杯,下次见!


原文链接:https://www.freecodecamp.org/news/how-to-manage-state-in-a-react-app/

作者:Germán Cocca

译者:Jing Wu

我在阿里招前端,我该怎么帮你?(现在还可以加模拟面试群)
如何拿下阿里巴巴 P6 的前端 Offer
如何准备阿里P6/P7前端面试--项目经历准备篇
大厂面试官常问的亮点,该如何做出?
如何从初级到专家(P4-P7)打破成长瓶颈和有效突破
若川知乎问答:2年前端经验,做的项目没什么技术含量,怎么办?

如何准备20K+的大厂前端面试

fb5211e2e3393c4828617e7c5dcf8290.gif

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经坚持写了8年,点击查看年度总结。
同时,最近组织了源码共读活动,帮助4000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。

065042ceae653eeaae33d6a1a8554383.jpeg

扫码加我微信 lxchuan12、拉你进源码共读

今日话题

目前建有江西|湖南|湖北 籍 前端群,想进群的可以加我微信 lxchuan12 进群。分享、收藏、点赞、在看我的文章就是对我最大的支持~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/274544.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

长语音识别体验_如何为语音体验写作

长语音识别体验重点 (Top highlight)“Voice User Interface (VUI) Designer” is an increasingly prominent job title in the tech world. A VUI designer typically writes the conversation and designs the flow between a VUI — an invisible interface that communica…

分析了1011个程序员的裁员情况后得出的启示

大家好&#xff0c;我是若川。持续组织了近一年的源码共读活动&#xff0c;感兴趣的可以 点此扫码加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系…

定义设计系统

System is “the whole creation, the universe,” from Late Latin systema “an arrangement, system,” from Greek systema “organized whole, a whole compounded of parts”.系统是晚期拉丁语系统的“整体创造物&#xff0c;宇宙”&#xff0c;是希腊语系统的“一种安排…

2w行代码、200个实战项目,助你修炼5大编程基本功。【送书《设计模式之美》】...

大家好&#xff0c;我是若川。之前送了很多书&#xff0c;现在又和异步图书合作再次争取了几本书&#xff0c;具体送书规则看文末。所谓练武不练功&#xff0c;到老一场空&#xff0c;以技术为驱动的程序员同样如此。面向对象编程范式、设计原则、代码规范、重构技巧和设计模式…

C++第10周项目2扩展之2参考——迭代求和

课程首页地址&#xff1a;http://blog.csdn.net/sxhelijian/article/details/7910565【项目2扩展之2&#xff08;选做&#xff09;】计算下面的式子&#xff0c;不能使用求幂函数pow()式一&#xff1a;#include <iostream> using namespace std; int main( ) { int i,m1;…

swift自行车品牌介绍_品牌101:简介

swift自行车品牌介绍Sometimes when I’m around designer friends and there’s a lull in the conversation one of us will blurt out, “What is branding, anyway?” Then we shrug our shoulders and chuckle, knowing that the answer is far too complex to sum up in…

flutter 透明度动画_Flutter中的动画填充+不透明度动画✨

flutter 透明度动画Flutter SDK provides us with many widgets which help us in animating elements on screen easily by implicitly managing the animations i.e. we need not worry about creating and managing intances of AnimationController during the lifecycle o…

阿里 P10 是怎样的存在?

谈起中国顶尖的程序员&#xff0c;很多人首先会想到之前的雷军、张小龙&#xff0c;还有现在的多隆、行癫、道哥等人&#xff0c;但今天我想聊一聊的这位大神&#xff0c;他的技术成就也同样令人瞩目。19 年获得国家技术发明二等奖、20 年获得国家计算机协会颁发的“ CCF 杰出工…

vba交付图表设计_您是在为交付目的而“设计”吗?

vba交付图表设计重点 (Top highlight)It’s a regular Monday morning. All the design team is organizing the tasks for the ongoing week and reviewing requirements and deadlines for the various projects at the studio or company you work at. Suddenly, among the …

前端必读书籍推荐

大家好&#xff0c;我是若川。持续组织了近一年的源码共读活动&#xff0c;感兴趣的可以 点此扫码加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系…

window程序设计学会_是时候我们学会设计合适的饼图了

window程序设计学会Pie charts are common in data science — next to the 饼形图在数据科学中很常见- bar chart and the line plot, the pie chart is incredibly standard and simple. A circle is split into several slices, with each slice’s angle representing how…

「非广告」程序员如何才能尽量避免被裁?

大家好&#xff0c;我是若川。持续组织了近一年的源码共读活动&#xff0c;感兴趣的可以 点此扫码加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系…

使用css制作三角,兼容IE6,用到的标签divsspan

使用css来制作三角&#xff0c;在日常用得较多。恰好这几天项目中有用到&#xff0c;之前只是从网上copy下来代码直接用&#xff0c;但是今天在用的时候遇到一些问题&#xff0c;于是借此机会把这个css绘制三角好好研究下吧。 我们分别写一个<div>,<s>,<span>…

培训师 每小时多少钱_每个产品设计师需要了解的品牌知识

培训师 每小时多少钱重点 (Top highlight)These days, it pays to know about brand. The rise of startups has created thousands of new brand design opportunities, and people of all disciplines are working to help brands compete in a crowded world. Increasingly,…

Android 绑定远程服务出现 Not Allowed to bind service

E/AndroidRuntime(3783): Caused by: java.lang.SecurityException: Not allowed to bind to service Intent { actcom.lenovo.pleiades.conntek.pad } 主要原因是服务中设有签名保护&#xff0c;该服务上一次是通过A设备中的Eclipse签名的&#xff0c;这一次是通过B设备中的Ec…

axios 发布 v1.1.0 据说导致很多网站瘫痪~那么如何自动提升版本号呢~

- 大家好&#xff0c;我是若川。友情提醒&#xff0c;今天还是周二。就不发长篇技术文了~近日&#xff0c;axios 发布了 v1.1.0 版本&#xff0c;调用 axios.get 时报错&#xff0c;据说导致请求无效很多网站瘫痪。目前官方已发布了 v1.1.1 v1.1.2 修复了该问题。让我想起群友在…

七月时忙碌而充实的_如何减少忙碌而更有效

七月时忙碌而充实的In our hectic modern world, we believe that rushing from one task to the next and managing multiple priorities shows everyone that we are productive.在忙碌的现代世界中&#xff0c;我们相信从一项任务过渡到下一项任务并处理多项优先事项可以向所…

github上阅读源码很费劲?你可能不知道这几个神器

大家好&#xff0c;我是若川。今天周四&#xff0c;再熬一天就可以放假了。有时候在github浏览到合适的仓库时&#xff0c;总想着打开看看源码&#xff0c;但又不想克隆。推荐几个在线编辑器打开github仓库的网站~最后欢迎大家在文末投票&#xff0c;看看大概有多少人知道或者不…

Codeforces Round #149 (Div. 2)【AK】

吐槽&#xff1a;比赛刚开始codeblocks出了点问题。。边看题边弄编译器。。。囧。。 D居然一直没看。。因为E题意好懂。。然后sb地卡了一场E。。。战斗力太不稳定。。。 A... A 1 #include<cstdio>2 #include<cstring>3 #include<iostream>4 #define N 10001…

ui设计卡片阴影_UI设计形状和对象基础知识:阴影和模糊

ui设计卡片阴影第三部分 (Part 3) Welcome to the third part of the UI Design super-basics. This time we’ll cover two of the most commonly used effects — shadows and blurs.欢迎使用UI设计超级基础的第三部分。 这次我们将介绍两种最常用的效果- 阴影和模糊 。 Und…