Adios Redux:有效使用 React Hooks 和 Context

2025-05-24

Adios Redux:有效使用 React Hooks 和 Context

Ciao%20Redux/undraw_active_options_8je6.png

2020年了,React 仍然是全球最受欢迎的前端框架。这不仅仅是因为它相对简单,而是它持续改进的事实让我一直着迷(无意的双关)。Hooks 的引入将 React 生态系统从基于类的组件转变为函数式组件,让编写 React 变得更加有趣。但目前为止,还没有一个特定的状态管理工具成为 React 的首选。

Redux 确实很流行。但 Redux 最大的缺点在于它一开始很难学,因为有很多样板代码。最近我看到一些推文

这让我开始疯狂学习,并且我了解了一些令人兴奋的模式和包,它们可能会彻底改变你对钩子和全局状态的看法(对我来说确实如此)。

最初想写这个系列文章的时候,我脑子里有太多标题可选了。比如《状态管理 2020》《React 中的自定义 Hooks》等等。但最终我决定用 Ciao Redux(再见 Redux),因为这似乎是这个系列文章的最终目标。

本文的灵感来自 Tanner Linsley 在 JSConf Hawaii 2020 上的精彩演讲。如果您还没有看过,我建议您观看。

那么让我们开始吧。

你如何看待React 中的状态

有人会简单地说,状态是前端存在的所有数据,或者你从服务器获取的数据。但是,当你使用 React 构建应用程序一段时间后,你就会明白我的意思。

状态主要可分为两种类型:

  • UI 状态
  • 服务器缓存

你可能想知道我在说什么。让我解释一下。

UI 状态是用于管理 UI 的状态或信息。例如,深色/浅色主题、下拉菜单切换、表单错误状态管理等。服务器缓存是从服务器接收的数据,例如用户详情、产品列表等。

Ciao%20Redux/state.png

管理国家

让我们从基础开始。顺便构建一些示例。不,不是待办事项列表。我们已经有足够多的教程了。我们将构建一个包含登录屏幕和主屏幕的简单应用程序。

useState

钩子useState允许我们在函数式组件内部使用状态。这样就不用再费力地在构造函数中声明状态,并通过 访问它了this。只需这样做

import { useState } from 'react'

const [name, setName] = useState("")
Enter fullscreen mode Exit fullscreen mode

我们得到name变量和一个函数来更新变量setName

现在让我们利用这些知识为我们的页面制作一个登录表单。

import React, { useState } from 'react'

export default function Login() {
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")
  const [emailError, setEmailError] = useState(false)
  const [passwordError, setPasswordError] = useState(false)
    const [isLoading, setIsLoading] = useState(false)

    async function handleSubmit() {
        setIsLoading(true)
        const res = await axios.post(url, {email, password})
        if(res.data.status === "EMAIL_ERROR") {
      setEmailError(true)
    }
    if(res.data.status === "PASSWORD_ERROR") {
      setPasswordError(true)
    }
    // Otherwise do stuff
    }
    return (
        <div>
            <input 
                type="text"
                value={email} 
                onChange={
                    e => setEmail(e.target.value)
                } 
            />
            {emailError && <ErrorComponent type="EMAIL_ERROR" />}
            <input 
                type="password" 
                value={password}
                onChange={
                    e => setPassword(e.target.value)
                } 
            />
            {passwordError && <ErrorComponent type="PASSWORD_ERROR" />}
            { isLoading
            ? <button onClick={() => handleSubmit()}>Sign in</button>
            : <LoadingButton /> }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

这可行。但这肯定不是最好的方法,对吧?而且,如果再添​​加一些其他因素或验证检查,就很容易失控。

useReducer

熟悉 Redux 的人肯定知道useReducer它的工作原理。对于不熟悉 Redux 的人,下面是它的工作原理。

Action -------> Dispatch -------> Reducer --------> Store
Enter fullscreen mode Exit fullscreen mode

你创建一个 action 并 dispatch 它,它会经过 reducer 并更新 store。让我们在前面的例子中实现它,看看它是如何工作的。

import React, { useReducer } from 'react'

const initialState = {
  user: {
    email: "",
    password: ""
  },
  errors: {
    email: false,
    password: false
  },
    isLoading: false
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'CHANGE_VALUE':
      return {
        ...state,
        user: {
          ...state.user,
          [action.field]: action.data
        }
      }
    case 'ERROR':
      return {
        ...state,
        errors: {
          ...state.errors,
          [action.type]: true
        }
      }
    case 'LOADING':
      return {
    ...state,
    isLoading: true
      }
    default:
      return state
  }
} 

export default function Login() {
  const [state, dispatch] = useReducer(reducer, initialState)

  async function handleSubmit() {
        dispatch({type: 'LOADING'})
        const res = await axios.post(url, store.user)
        if(res.data.status === "EMAIL_ERROR") {
      dispatch({type: 'ERROR', field: "email"})
    }
    if(res.data.status === "PASSWORD_ERROR") {
      dispatch({type: 'ERROR', field: "password"})
    }
    // Otherwise do stuff
    }

    return (
        <div>
            <input 
                type="text"
                value={state.user.email} 
                onChange={
                    e => dispatch({type: "CHANGE_VALUE", data: e.target.value, field: "email"})
                } 
            />
            {state.errors.email && <ErrorComponent type="EMAIL_ERROR" />}
            <input 
                type="password" 
                onChange={
                                        value={state.user.password}
                    e => dispatch({type: "CHANGE_VALUE", data: e.target.value, field: "password"})
                } 
            />
            {state.errors.password && <ErrorComponent type="PASSWORD_ERROR" />}
            <button onClick={() => handleSubmit()}>Sign in</button>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

这看起来不错,我们不用处理单独的函数,只需声明一个 Reducer,并定义一些 Action 和相应的 Store 变更即可。这非常实用,因为在使用时useState,随着需求的增长,我们很容易忘记变量的数量。你一定注意到了,这段代码比之前的代码长得多,这让我们进入下一部分。

从 UI 中抽象逻辑

在 React 中开发应用程序时,应始终尝试将业务逻辑与 UI 代码分开。与用户交互的 UI 组件应该只知道用户可以执行哪些交互(操作)。此外,这还能为您的代码库提供合理的结构和良好的可维护性。Redux 很好地支持了这一点,我们可以在其他地方定义操作,由它来处理所有逻辑,从而保持 UI 代码的简洁。但是,如何使用 Hooks 来实现这一点呢?自定义 Hooks 来帮您!

自定义钩子

React 允许你创建自定义 hooks,以便更好地在组件之间分离和共享逻辑。对于上面的示例,我们可以创建一个名为hooks/useLoginReducer.js

import { useReducer } from 'react'

const initialState = {
  user: {
    email: "",
    password: ""
  },
  errors: {
    email: false,
    password: false
  },
    isLoading: false
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'CHANGE_VALUE':
      return {
        ...state,
        user: {
          ...state.user,
          [action.field]: action.data
        }
      }
    case 'ERROR':
      return {
        ...state,
        errors: {
          ...state.errors,
          [action.type]: true
        }
      }
    case 'LOADING':
      return {
    ...state,
    isLoading: true
      }
    default:
      return state
  }
} 

export default function useLoginReducer() {
  const [store, dispatch] = useReducer(reducer, initialState)
  return [store, dispatch]
}
Enter fullscreen mode Exit fullscreen mode

然后在登录组件中

import React from 'react'
import useLoginReducer from '../hooks/useLoginReducer'

export default function Login() {
  const [store, dispatch] = useLoginReducer()
    ...
}
Enter fullscreen mode Exit fullscreen mode

瞧!我们把逻辑和组件分离了,现在看起来简洁多了。自定义钩子可以很好地实现关注点分离。

让我们进入最精彩的部分。

全局状态

像 Redux 这样的第三方库旨在提供全局状态管理,因为 prop 钻取实在太麻烦了。React 拥有 Context API,允许在组件之间传递数据。Context 允许你声明一个Provider用于存储或初始化数据,以及Consumer用于读取或更新数据的组件。Redux 在后台使用它,但是

  • 很长一段时间都不稳定
  • 需要渲染 props,导致可读性降低

然而,随着 React Hooks 的引入,使用 context 变得容易得多。我们可以轻松地声明全局状态,并通过组合hooks和来使用它们context。让我们看一下上面使用的示例。假设登录后,你想用用户的详细信息更新全局存储,这些信息可以在 Navbar 组件中显示用户的名称。

我们首先声明一个上下文,然后使用钩子来存储和更新数据。

const globalContext = React.createContext()

const intialState = {
    user: {
        ...
    }
}

const reducer = {
    ...
}

export const StoreProvider = ({children}) => {
  const [store, dispatch] = React.useReducer(reducer, initialState)

    //memoizes the contextValue so only rerenders if store or dispatch change
    const contextValue = React.useMemo(
        () => [store, dispatch],
        [store, dispatch]
    )

  return (
    <globalContext.Provider value={contextValue}>
      {children}
    </globalContext.Provider>
  )
}

export function useStore() {
  return React.useContext(globalContext)
}
Enter fullscreen mode Exit fullscreen mode

那么让我通过这里的代码来解释一下。我们首先创建一个上下文。然后在组件中使用 useReducer 来创建 store 和 dispatch 方法。我们使用 useReduceruseMemo创建一个上下文变量,仅当其中一个依赖项发生变化时才会更新。然后,我们返回context.Provider带有上下文变量值的组件。在最后一部分,我们使用了useContexthook,它允许我们在函数式组件中使用上下文,前提是它位于Provider.

// App.js
import React from 'react';
import { StoreProvider, useStore } from './context';

function App() {
  return (
    <StoreProvider>
      <Navbar />
      ...
    </StoreProvider>
  );
}

// Login.js
import React from 'react';
import { useStore } from './context'

function Login() {
    const [, dispatch] = useStore()
    ...
    function handleSubmit() {
        ...
        dispatch(...)
    }
}

// Navbar.js
import React from 'react';
import { useStore } from './context';

function Navbar() {
    const [{user}, dispatch] = useStore()
    return (
        ...
        <li>{user.name}</li>
    )
}
Enter fullscreen mode Exit fullscreen mode

因此,我们将应用程序组件包装在其中StoreProvider,并使用useStore返回的函数访问嵌套组件中的存储值和调度函数。听起来很棒吧。嗯,其实不然。这其中有很多问题。我们来看看。

  • 首先,由于我们同时导出了storedispatch。任何更新组件(仅使用 dispatch)且不使用 store 的组件也会在每次状态更改时重新渲染。这是因为每次 context 值更改时都会生成一个新的数据对象。这是不可取的。
  • 其次,我们为所有组件使用同一个存储。当我们向 Reducer 的 initialState 添加任何其他状态时,数据量都会大幅增长。此外,所有使用该上下文的组件都会在状态发生变化时重新渲染。这是不可取的,可能会破坏你的应用程序。

那么我们该如何解决这些问题呢?几天前,我偶然看到了这条推文

问题解决了。这就是我们需要的。现在我们来实现它,我会解释一下。

对于第一个问题,我们可以简单地将存储和调度分离到不同的上下文中,用于DispatchContext更新存储和StoreContext使用存储。

const storeContext = React.createContext()
const dispatchContext = React.createContext()

const intialState = {
    user: {
        ...
    }
}

const reducer = {
    ...
}

export const StoreProvider = ({children}) => {
  const [store, dispatch] = React.useReducer(reducer, initialState)

  return (
    <dispatchContext.Provider value={dispatch}>
      <storeContext.Provider value={store}>
        {children}
      </storeContext.Provider>
    </dispatchContext.Provider>
  )
}

export function useStore() {
  return React.useContext(storeContext)
}

export function useDispatch() {
    return React.useContext(dispatchContext)
}
Enter fullscreen mode Exit fullscreen mode

然后我们只需根据我们的情况导入useDispatch即可。useStore

// App.js
import React from 'react';
import { StoreProvider } from './context';

function App() {
  return (
    <StoreProvider>
      <Navbar />
      ...
    </StoreProvider>
  );
}

//Login.js
import React from 'react';
import { useDispatch } from './context'

function Login() {
    const dispatch = useDispatch()
    ...
    function handleSubmit() {
        ...
        dispatch(...)
    }
}

// Navbar.js
import React from 'react';
import { useStore } from './context'

function Navbar() {
    const {user} = useStore()
    return (
        ...
        <li>{user.name}</li>
    )
}
Enter fullscreen mode Exit fullscreen mode

现在来看第二个问题。其实很简单,我们不需要创建一个单独的 store。我之前使用 context 时遇到的困难主要就是因为这个原因。即使在 Redux 中,我们也会将 Reducer 分离并组合起来。

我们可以简单地定义一个函数,它接受一个参数initialStatereducer返回一个 store。让我们看看它是怎么做的。

import React from 'react'

export default function makeStore(reducer, initialState) {
  const storeContext = React.createContext()
  const dispatchContext = React.createContext()

  const StoreProvider = ({children}) => {
    const [store, dispatch] = React.useReducer(reducer, initialState)

    return (
      <dispatchContext.Provider value={dispatch}>
        <storeContext.Provider value={store}>
          {children}
        </storeContext.Provider>
      </dispatchContext.Provider>
    )
  }

  function useStore() {
    return React.useContext(storeContext)
  }

  function useDispatch() {
    return React.useContext(dispatchContext)
  }

  return [StoreProvider, useStore, useDispatch]
}
Enter fullscreen mode Exit fullscreen mode

然后我们可以按如下方式声明userContext

import makeStore from '../store'

const initalState = {
  user: {
        ...
    }
}

const reducer = (state, action) => {
  switch (action.type) {
    ...
        ...
  }
}

const [
  UserProvider,
  useUserStore,
  useUserDispatch
] = makeStore(reducer, initalState)

export { UserProvider, useUserStore, useUserDispatch }
Enter fullscreen mode Exit fullscreen mode

最后在需要的时候使用它

// App.js
import React from 'react';
import { UserProvider } from './userStoreContext';

function App() {
  return (
    <UserProvider>
      <Navbar />
      ...
    </UserProvider>
  );
}

// Login.js
import React from 'react';
import { useUserDispatch } from './userStoreContext'

function Login() {
    const dispatch = useUserDispatch()
    ...
    function handleSubmit() {
        ...
        dispatch(...)
    }
}

// Navbar.js
import React from 'react';
import { useUserStore } from './userStoreContext'

function Navbar() {
  const {user} = useUserStore()
  return (
    ...
    <li>{user.name}</li>
  )
}
Enter fullscreen mode Exit fullscreen mode

完成。如果我们想要另一个 store,只需创建另一个 store 并将其包装在我们的应用或要使用它的组件中即可。例如

function App() {
  return (
    <UserProvider>
        <Navbar />
        <ProductProvider>
            <Products />
        </ProductProvider>
    </UserProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

哇哦!本系列的第一部分就到这里了。希望你已经学会了如何有效地使用钩子和上下文。在接下来的文章中,我将讨论react-query如何处理服务器缓存。敬请期待。

进一步阅读

文章来源:https://dev.to/ankitjey/ciao-redux-using-react-hooks-and-context-effectly-398j
PREV
将 React App 打造成渐进式 Web 应用 (PWA) 什么是 PWA?1. 注册 Service Worker 2. 立即更新 HTML 3. 激活 ServiceWorker 4. 创建 manifest.json 文件
NEXT
每个开发人员都应该知道的 3 个 Github 存储库 技术面试手册 free-for.dev