React状态管理进阶:用 useReducer 和 useContext优雅替代useState

106 阅读6分钟

useState的使用

前言

在React的世界里,useStateuseStateuseRef等是我们最早接触、最常用的Hooks,没有他们我们根本很难完成项目的开发,他们简单、直接,适合管理组件内部的局部状态。但日新月异的今天,useState像一名老者,垂垂老矣,面向企业级的复杂项目时,useState似乎有些力不从心。而useReduceruseContext便是两位强大的“援手”。

useState:小而美的状态管理

useState的优势就在于简单且直观。它适合一些简单独立的状态。

const [count, setCount] = useState(0);

不适于复杂的状态,比如许许多多不同的action更新不同的state,或者多个组件需要共享同一份状态的时候,useState就会显得相当笨重,代码也变得很混乱,可维护性差

假设我们有一个表单,包含计数器、用户名、登录等多种状态,伪代码就会是:

function ProfileForm() {
  const [count, setCount] = useState(0);
  const [username, setUsername] = useState('');
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [theme, setTheme] = useState('light');
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const handleLogin = () => setIsLoggedIn(true);
  const handleLogout = () => setIsLoggedIn(false);
  const handleThemeChange = (e) => setTheme(e.target.value);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>加一</button>
      <button onClick={decrement}>减一</button>
      <input
        value={username}
        onChange={e => setUsername(e.target.value)}
        placeholder="用户名"
      />
      <button onClick={handleLogin} disabled={isLoggedIn}>登录</button>
      <button onClick={handleLogout} disabled={!isLoggedIn}>登出</button>
      <select value={theme} onChange={handleThemeChange}>
        <option value="light">浅色</option>
        <option value="dark">深色</option>
      </select>
      <p>当前主题:{theme}</p>
      <p>登录状态:{isLoggedIn ? '已登录' : '未登录'}</p>
      <p>用户名:{username}</p>
    </div>
  );
}

useReducer:复杂状态的“指挥官”

useReducer 的出现,正是为了解决复杂状态管理的问题。它的思想类似于 Redux,通过 action 和 reducer 来集中管理状态变化。这样,状态的变更逻辑更加清晰、可维护

基本用法:

const [state, dispatch] = useReducer(reducer, initialState);

其中state表示当前的状态对象,dispatch是派发action的函数,用于触发状态的更新,reducer是一个纯函数,接受当前state和action,返回state,initialState就是字面初始状态的意思。

参数1:state(当前状态)

state就是当前组件的状态数据,可以理解为就是我们熟知的useState返回的state,state由useReducer返回,其值会随着dispatch的action发生变化,用来渲染UI或者作为业务逻辑的依据。

const initialState = {
  count: 0,
  username: '',
  isLoggedIn: false,
  theme: 'light'
};

比如在上面demo中,state就是theme,isLogged,count,username等,可以通过state.username、state.theme来访问。

参数2:关于dispatch(派发函数)

dispatch(action)就是用来派发action,触发reducer执行,进而更新state的函数,比如下面代码,按钮中触发点击事件,用dispatch派发action(通常是一个对象,至少包含type,还可以用payload携带数据),进而触发reducer执行加一/减一操作。

<button onClick={() => dispatch({ type: 'increment' })}>加一</button>
<button onClick={() => dispatch({ type: 'decrement' })}>减一</button>

参数3:reducer(状态处理函数)

由上面的参数2可以知道,reducer是一个状态处理函数,reducer的概念是伴随Redux的出现在JS中逐渐流行起来,但学习reducer并不需要去学习Redux,简单点就是它本质上是一个纯函数(没有任何UI和副作用,相同的输入输出),接收当前的state和action,加一操作可以按count => count + 1 处理,返回新的state对象,因此我们通过reducer函数就可以很容易知道state的变化。

参数4: initialState(初始状态)

initialState很好理解就是字面的初始状态的意思,它是组件状态的初始值,它可以是任意的类型(对象、数组、数字、字符串等),但通常是对象,便于管理多个字段,当useReducer第一次执行时,会用initialState作为state的初始值。

const initialState = {
  count: 0,
  username: '',
  isLoggedIn: false,
  theme: 'light'
};

于是就可以对上面代码进行改写为下面的伪代码:

const initialState = {
  count: 0,
  username: '',
  isLoggedIn: false,
  theme: 'light'
};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'decrement':
      return { ...state, count: state.count - 1 };
    case 'setUsername':
      return { ...state, username: action.payload };
    case 'login':
      return { ...state, isLoggedIn: true };
    case 'logout':
      return { ...state, isLoggedIn: false };
    case 'setTheme':
      return { ...state, theme: action.payload };
    default:
      throw new Error('未知 action');
  }
}

function ProfileForm() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>加一</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>减一</button>
      <input
        value={state.username}
        onChange={e => dispatch({ type: 'setUsername', payload: e.target.value })}
        placeholder="用户名"
      />
      <button onClick={() => dispatch({ type: 'login' })} disabled={state.isLoggedIn}>登录</button>
      <button onClick={() => dispatch({ type: 'logout' })} disabled={!state.isLoggedIn}>登出</button>
      <select
        value={state.theme}
        onChange={e => dispatch({ type: 'setTheme', payload: e.target.value })}
      >
        <option value="light">浅色</option>
        <option value="dark">深色</option>
      </select>
      <p>当前主题:{state.theme}</p>
      <p>登录状态:{state.isLoggedIn ? '已登录' : '未登录'}</p>
      <p>用户名:{state.username}</p>
    </div>
  );
}

上述我们定义的6个state来描述页面的状态,可以预见当更多状态加入到页面,这个页面可想而知有多乱,出现一个bug更是一个噩梦了。

useContext:跨组件的“传送带”

useContext 则解决了“组件传参地狱”的问题。当多个组件需要访问同一份数据时,useContext 可以让我们轻松地在组件树中传递数据,而无需层层 props 传递。

// 创建Context
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext('light');

// 提供Context
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// ThemeSwitcher.jsx 在子组件中消费 Context
function ThemeSwitcher() {
  const { theme, setTheme } = useContext(ThemeContext);
  return (
    <div>
      <span>当前主题:{theme}</span>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        切换主题
      </button>
    </div>
  );
}

// App.jsx 应用Provider
function App() {
  return (
    <ThemeProvider>
      <ThemeSwitcher />
      {/* 其他组件 */}
    </ThemeProvider>
  );
}

在App中,被<ThemeProvider>包裹的<ThemeSwitcher />和其他组件,都可以共享消费Context,比如ThemeSwitcher中可以直接访问到theme,让我们轻松地在组件树中传递数据,而无需层层props传递

useReducer与useContext:状态管理之神!

useReducer与useContext的全局状态管理

使用useReducer与useContext后上面的demo就变成了:

// 1. 创建 context
const AppContext = createContext();

const initialState = {
  count: 0,
  username: '',
  isLoggedIn: false,
  theme: 'light'
};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'decrement':
      return { ...state, count: state.count - 1 };
    case 'setUsername':
      return { ...state, username: action.payload };
    case 'login':
      return { ...state, isLoggedIn: true };
    case 'logout':
      return { ...state, isLoggedIn: false };
    case 'setTheme':
      return { ...state, theme: action.payload };
    default:
      throw new Error('未知 action');
  }
}

// 2. Provider 组件
export function AppProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
}

function ProfileForm() {
  const { state, dispatch } = useAppContext();

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>加一</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>减一</button>
      <input
        value={state.username}
        onChange={e => dispatch({ type: 'setUsername', payload: e.target.value })}
        placeholder="用户名"
      />
      <button onClick={() => dispatch({ type: 'login' })} disabled={state.isLoggedIn}>登录</button>
      <button onClick={() => dispatch({ type: 'logout' })} disabled={!state.isLoggedIn}>登出</button>
      <select
        value={state.theme}
        onChange={e => dispatch({ type: 'setTheme', payload: e.target.value })}
      >
        <option value="light">浅色</option>
        <option value="dark">深色</option>
      </select>
      <p>当前主题:{state.theme}</p>
      <p>登录状态:{state.isLoggedIn ? '已登录' : '未登录'}</p>
      <p>用户名:{state.username}</p>
    </div>
  );
}

import React from 'react';
import ReactDOM from 'react-dom';
import { AppProvider } from './AppContext'; // 假设上面代码在 AppContext.js
import ProfileForm from './ProfileForm';

ReactDOM.render(
  <AppProvider>
    <ProfileForm />
    {/* 其他需要全局状态的组件 */}
  </AppProvider>,
  document.getElementById('root')
);

总结

至此简单介绍了React中 useState、useReducer和useContext 三大常用Hooks的使用场景与优缺点。

useState适合管理简单、独立的局部状态,但在复杂或多状态场景下会导致代码混乱、难以维护。

useReducer 通过集中管理状态和变更逻辑,适合处理复杂状态,提升了代码的可维护性。

useContext 则解决了多组件间数据共享和“props传递地狱”的问题。

详细请参考

  1. React 官方文档
  1. Redux 官方文档:Redux Fundamentals, Part 2: Concepts and Data Flow