如何选择正确的React状态管理解决方案

171 阅读10分钟

状态管理是每个开发者在构建React应用时面临的基本挑战--而且这不是一个微不足道的挑战。在React中,有许多有效的方法来管理状态,每一种方法都解决了一系列突出的问题。

作为开发者,不仅要意识到不同的方法、工具和模式,而且要理解它们的使用情况和权衡。

思考状态管理的一个有用的方法是我们在项目中解决的问题。在这篇文章中,我们将介绍React中管理状态的常见用例,并了解何时应该考虑使用每种解决方案。让我们通过建立一个简单的计数器应用来完成这个任务。

React中的本地组件状态

实现计数器的最简单方法是使用本地组件状态与useState Hook。

import { useState } from 'react'

const Counter = () => {
    const [count, setCount] = useState(0)

    const increaseCount = () => {
        setCount(count + 1)
    }

    const decreaseCount = () => {
        if (count > 0) {
            setCount(count - 1)
        }
    }
    return (
        <div>
            <h1>{count}</h1>
            <button onClick={decreaseCount}>-</button>
            <button onClick={increaseCount}>+</button>
        </div>
    )
}

export default Counter

那么我们就完成了,对吗?文章结束了?不尽然。

如果这是一个真实的项目,很可能在未来,我们会在应用的其他地方需要更多的按钮和标题。确保它们的外观和行为一致是个好主意,这就是为什么我们应该把它们变成可重用的React组件。

React中的组件props

将我们的ButtonHeader 变成独立的组件,就会发现一个新的挑战。我们需要一些方法在它们和主Counter 组件之间进行交流。

这就是组件props发挥作用的地方。对于我们的Header 组件,我们添加一个text 的道具。对于我们的Button ,我们需要一个label 道具和一个onClick 回调。我们的代码现在看起来像这样。

import { useState } from 'react'

const Header = ({ text }) => <h1>{text}</h1>

const Button = ({ label, onClick }) => (
    <button onClick={onClick}>{label}</button>
)

const Counter = () => {
    const [count, setCount] = useState(0)

    const increaseCount = () => {
        setCount(count + 1)
    }

    const decreaseCount = () => {
        if (count > 0) {
            setCount(count - 1)
        }
    }
    return (
        <div>
            <Header text={count} />
            <Button onClick={decreaseCount} label="-" />
            <Button onClick={increaseCount} label="+" />
        </div>
    )
}

export default Counter

这看起来很好!但是,想象一下下面的情况:如果我们要做的事情是,在我们的网站上添加一个 "道具"。但是想象一下下面的情况:如果我们只需要在我们的主页上显示计数,并且有一个单独的路由/controls ,在那里我们同时显示计数和控制按钮,那该怎么办?我们应该怎么做呢?

React中的路由

鉴于我们正在建立一个单页应用程序,现在有第二个状态我们需要处理 - 我们所处的路由。让我们看看如何用React Router来完成这个工作,比如说。

import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'
import { useState } from 'react'

const Header = ({ text }) => <h1>{text}</h1>

const Button = ({ label, onClick }) => (
    <button onClick={onClick}>{label}</button>
)

const Home = ({ count }) => {
    return <Header text={text} />
}

const Controls = ({ count, decreaseCount, increaseCount }) => {
    return (
        <>
            <Header text={text} />
            <Button onClick={decreaseCount} label="-" />
            <Button onClick={increaseCount} label="+" />
        </>
    )
}

const App = () => {
    const [count, setCount] = useState(0)
    const increaseCount = () => {
        setCount(count + 1)
    }
    const decreaseCount = () => {
        if (count > 0) {
            setCount(count - 1)
        }
    }

    return (
        <Router>
            <nav>
                <Link to="/">Home</Link>
                <Link to="/controls">Controls</Link>
            </nav>
            <Switch>
                <Route path="/controls">
                    <Controls
                        increaseCount={increaseCount}
                        decreaseCount={decreaseCount}
                        count={count}
                    />
                </Route>
                <Route path="/">
                    <Home count={count} />
                </Route>
            </Switch>
        </Router>
    )
}

export default App

很好!我们现在有了独立的路由,一切都按预期工作。然而,你可能会注意到一个问题。我们将计数状态保存在App ,并使用props将其传递到组件树下。但是,我们似乎一次又一次地传递同一个道具,直到我们到达需要使用它的组件。当然,随着我们应用程序的增长,情况只会越来越糟。这就是所谓的道具钻孔。

让我们来解决这个问题!

使用上下文+useReducer

如果有一种方法可以让我们的组件访问count ,而不必通过道具来接收状态,那不是很好吗?React Context API和useReducer Hook的组合就可以做到这一点。

import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'
import { createContext, useContext, useReducer } from 'react'

const initialState = 0

const reducer = (state, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1
        case 'DECREMENT':
            return state - 1 >= 0 ? state - 1 : 0
        default:
            return state
    }
}

const CountContext = createContext(null)

const useCount = () => {
    const value = useContext(CountContext)
    if (value === null) throw new Error('CountProvider missing')
    return value
}

const CountProvider = ({ children }) => (
    <CountContext.Provider value={useReducer(reducer, initialState)}>
        {children}
    </CountContext.Provider>
)

const Header = ({ text }) => <h1>{text}</h1>

const Button = ({ label, onClick }) => (
    <button onClick={onClick}>{label}</button>
)

const Home = () => {
    const [state] = useCount()
    return <Header text={state} />
}

const Controls = () => {
    const [state, dispatch] = useCount()
    return (
        <>
            <Header text={state} />
            <Button onClick={() => dispatch({ type: 'DECREMENT' })} label="-" />
            <Button onClick={() => dispatch({ type: 'INCREMENT' })} label="+" />
        </>
    )
}

const App = () => {
    return (
        <CountProvider>
            <Router>
                <nav>
                    <Link to="/">Home</Link>
                    <Link to="/controls">Controls</Link>
                </nav>
                <Switch>
                    <Route path="/controls">
                        <Controls />
                    </Route>
                    <Route path="/">
                        <Home />
                    </Route>
                </Switch>
            </Router>
        </CountProvider>
    )
}

export default App

真棒!"。我们已经解决了道具钻取的问题。我们通过创建一个描述性的还原器,使我们的代码更具声明性,从而得到了额外的分数。

我们对我们的实现很满意,而且对于许多用例来说,这确实是我们所需要的。但是,如果我们能够持久化计数,使其在每次刷新页面时不被重置为0,那不是很好吗?并且有一个应用程序状态的日志?崩溃报告呢?

如果能知道我们的应用程序在崩溃时的确切状态,以及如何利用惊人的开发工具,这将是非常有帮助的。好吧,我们完全可以用Redux来做到这一点!

使用Redux进行状态管理

通过使用Redux来管理我们应用程序的状态,我们可以做到以上所有的事情,甚至更多。该工具背后有一个强大的社区和一个丰富的生态系统,可以轻松利用。

让我们用Redux Toolkit设置我们的计数器。

import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'
import { configureStore, createSlice } from '@reduxjs/toolkit'
import { useSelector, useDispatch, Provider } from 'react-redux'

const counterSlice = createSlice({
    name: 'counter',
    initialState: {
        value: 0,
    },
    reducers: {
        increment: state => {
            state.value += 1
        },
        decrement: state => {
            if (state.value > 0) {
                state.value -= 1
            }
        },
    },
})

const store = configureStore({
    reducer: { counter: counterSlice.reducer },
})

const { increment, decrement } = counterSlice.actions

const Header = ({ text }) => <h1>{text}</h1>

const Button = ({ label, onClick }) => (
    <button onClick={onClick}>{label}</button>
)

const Home = () => {
    const count = useSelector(state => state.counter.value)
    return <Header text={count} />
}

const Controls = () => {
    const count = useSelector(state => state.counter.value)
    const dispatch = useDispatch()
    return (
        <>
            <Header text={count} />
            <Button onClick={() => dispatch(decrement())} label="-" />
            <Button onClick={() => dispatch(increment())} label="+" />
        </>
    )
}

const App = () => {
    return (
        <Provider store={store}>
            <Router>
                <nav>
                    <Link to="/">Home</Link>
                    <Link to="/controls">Controls</Link>
                </nav>
                <Switch>
                    <Route path="/controls">
                        <Controls />
                    </Route>
                    <Route path="/">
                        <Home />
                    </Route>
                </Switch>
            </Router>
        </Provider>
    )
}
export default App

这看起来真的很整洁我们的状态现在存储在全局的Redux存储中,并用纯函数进行管理(Redux Toolkit在引擎盖下使用Immer来保证不变性)。我们已经可以利用强大的Redux DevTools的优势了。

但是像处理副作用,或使状态持久化,或实现日志和/或崩溃报告这样的事情呢?这就是我们前面提到的Redux生态系统发挥作用的地方。

有多种选择来处理副作用,包括redux-thunkredux-saga。像redux-persist这样的库很适合将redux存储的数据保存在本地或会话存储中,使其持久化。

简而言之,Redux是伟大的!它被广泛地应用于Reaux中。它在React世界中被广泛使用,这是有原因的。

但是,如果我们更喜欢一种更分散的状态管理方法呢?也许我们担心性能问题,或者在React树的不同分支有频繁的数据更新,所以我们想避免不必要的重读,同时保持一切同步。

或者,我们需要一个好的方法来从我们的状态中获取数据,并在客户端高效、稳健地进行计算。如果我们想在不牺牲应用范围内的状态观察能力的情况下实现这一切呢?进入Recoil。

原子状态与Recoil

如果说我们能够通过一个简单的Counter应用来达到React Context或Redux的极限,这有点牵强。对于一个更好的原子状态管理用例,请看Dave McCabe关于Recoil 的精彩视频

尽管如此,用原子的方式思考状态确实有助于扩大我们对状态管理的词汇量。另外,Recoil API也很有趣,所以让我们用它重新实现我们的计数器。

import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'
import { atom, useRecoilState, RecoilRoot } from 'recoil'

const countState = atom({
    key: 'count',
    default: 0,
})

const Header = ({ text }) => <h1>{text}</h1>

const Button = ({ label, onClick }) => (
    <button onClick={onClick}>{label}</button>
)

const Home = () => {
    const [count] = useRecoilState(countState)
    return <Header text={count} />
}

const Controls = () => {
    const [count, setCount] = useRecoilState(countState)
    const increaseCount = () => {
        setCount(count + 1)
    }
    const decreaseCount = () => {
        if (count > 0) {
            setCount(count - 1)
        }
    }
    return (
        <>
            <Header text={count} />
            <Button onClick={decreaseCount} label="-" />
            <Button onClick={increaseCount} label="+" />
        </>
    )
}

const App = () => {
    return (
        <RecoilRoot>
            <Router>
                <div className="App">
                    <nav>
                        <Link to="/">Home</Link>
                        <Link to="/controls">Controls</Link>
                    </nav>
                    <Switch>
                        <Route path="/controls">
                            <Controls />
                        </Route>
                        <Route path="/">
                            <Home />
                        </Route>
                    </Switch>
                </div>
            </Router>
        </RecoilRoot>
    )
}

export default App

使用Recoil的感觉非常像使用React本身。回头看看我们最初的例子,就会发现这两者是多么的相似。Recoil也有它自己的一套开发工具。要记住的一个重要考虑是,这个库仍然是实验性的,会有变化。谨慎使用它。

好吧,我们可以有一个Recoil计数器。但状态管理的偏好取决于我们的优先级。如果应用程序是由一个团队建立的,而且当涉及到用户界面时,开发人员、设计师、项目经理和其他所有人都说同样的语言是非常重要的,怎么办?

此外,如果这种语言可以在我们的应用程序中用高度声明性的代码直接表达呢?如果我们能保证我们永远不会达到不可能的状态,从而消除一大类错误呢?你猜怎么着?我们可以。

XState的状态机

在状态图和状态机的帮助下,上述所有的事情都可以实现。状态图有助于可视化我们的应用程序的所有可能状态,并定义什么是可能的。它们很容易理解,分享,并在整个团队中讨论。

下面是我们的计数器的状态图。

Counter Is Allowed To Increase, It Is Not Possible For Counter To Decrease

虽然这只是一个微不足道的实现,但我们已经可以看到使用状态机的一个很酷的优势。最初,不可能递减计数器,因为它的初始值是0。这个逻辑是在我们的状态机上声明的,并在图表上可见,而在我们探索的其他方法中,一般来说,很难为它找到合适的位置。

下面是我们的状态机的实践。

import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'
import { useMachine } from '@xstate/react'
import { createMachine, assign } from 'xstate'

export const counterMachine = createMachine({
    initial: 'active',
    context: { count: 0 },
    states: {
        active: {
            on: {
                INCREMENT: {
                    actions: assign({ count: ctx => ctx.count + 1 }),
                },
                DECREMENT: {
                    cond: ctx => ctx.count > 0,
                    actions: assign({
                        count: ctx => ctx.count - 1,
                    }),
                },
            },
        },
    },
})

const Header = ({ text }) => <h1>{text}</h1>

const Button = ({ label, onClick }) => (
    <button onClick={onClick}>{label}</button>
)

const Home = () => {
    const [state] = useMachine(counterMachine)
    return <Header text={state.context.count} />
}

const Controls = () => {
    const [state, send] = useMachine(counterMachine)
    return (
        <>
            <Header text={state.context.count} />
            <Button onClick={() => send('DECREMENT')} label="-" />
            <Button onClick={() => send('INCREMENT')} label="+" />
        </>
    )
}

const App = () => {
    return (
        <Router>
            <nav>
                <Link to="/">Home</Link>
                <Link to="/controls">Controls</Link>
            </nav>
            <Switch>
                <Route path="/controls">
                    <Controls />
                </Route>
                <Route path="/">
                    <Home />
                </Route>
            </Switch>
        </Router>
    )
}

export default App

哇,这真是太棒了然而,我们在这里只是勉强接触到了状态机的表面。要了解更多关于它们的信息,请查看XState的文档。

好了,最后一个场景!如果我们简单的前端计数器应用有一个后台,会发生什么?如果我们需要与服务器通信以获取或修改计数,该怎么办?此外,如果我们想处理与数据获取相关的挑战,如异步性、加载状态、缓存和重新获取,该怎么办?

用React Query进行数据获取

我想强调的最后一个React状态管理工具是React Query。它是专门为使数据获取变得容易并解决上述问题(以及更多)而设计的。让我们看看它的运行情况。

import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'
import { ReactQueryDevtools } from 'react-query/devtools'
import axios from 'axios'
import {
    useQuery,
    useMutation,
    QueryClient,
    QueryClientProvider,
} from 'react-query'

const useCount = () => {
    return useQuery('count', async () => {
        const { data } = await axios.get('https://our-counter-api.com/count')
        return data
    })
}

const useIncreaseCount = () => {
    return useMutation(() =>
        axios.post('https://our-counter-api.com/increase', {
            onSuccess: () => {
                queryClient.invalidateQueries('count')
            },
        }),
    )
}

const useDecreaseCount = () => {
    return useMutation(
        () => axios.post('https://our-counter-api.com/descrease'),
        {
            onSuccess: () => {
                queryClient.invalidateQueries('count')
            },
        },
    )
}
const Header = ({ text }) => <h1>{text}</h1>

const Button = ({ label, onClick }) => (
    <button onClick={onClick}>{label}</button>
)

const Home = () => {
    const { status, data, error } = useCount()
    return status === 'loading' ? (
        'Loading...'
    ) : status === 'error' ? (
        <span>Error: {error.message}</span>
    ) : (
        <Header text={data} />
    )
}

const Controls = () => {
    const { status, data, error } = useCount()
    const increaseCount = useIncreaseCount()
    const decreaseCount = useDecreaseCount()

    return status === 'loading' ? (
        'Loading...'
    ) : status === 'error' ? (
        <span>Error: {error.message}</span>
    ) : (
        <>
            <Header text={data} />
            <Button onClick={() => decreaseCount.mutate()} label="-" />
            <Button onClick={() => increaseCount.mutate()} label="+" />
        </>
    )
}
const queryClient = new QueryClient()

const App = () => {
    return (
        <QueryClientProvider client={queryClient}>
            <Router>
                <ReactQueryDevtools />
                <nav>
                    <Link to="/">Home</Link>
                    <Link to="/controls">Controls</Link>
                </nav>
                <Switch>
                    <Route path="/controls">
                        <Controls />
                    </Route>
                    <Route path="/">
                        <Home />
                    </Route>
                </Switch>
            </Router>
        </QueryClientProvider>
    )
}

export default App

以上是一个相当幼稚的实现,有很多改进的余地。需要注意的是,我们可以很容易地进行服务器调用,对其进行缓存,并在需要时使缓存失效。此外,有了React Query,管理组件中的加载和错误状态的任务变得更加简单。

它是一个伟大的工具,可以与任何后端一起使用。如果你想知道如何用GraphQL设置它,请查看我的相关文章

总结

React的状态管理是一个广泛的话题。本文讨论的方法、模式和库的列表既不全面也不确定。我们的目标是要说明以特定方式解决特定问题背后的思考过程。

最后,React中的状态管理归结为意识到不同的选择,了解它们的好处和权衡,并最终选择最适合我们使用情况的解决方案。

编码愉快!✨

The postHow to choose the right React state management solutionappeared first onLogRocket Blog.