React 狀態管理 - useState-useReducer - useContext 實現全局狀態管理

useReducer 是 useState 的替代方案,用來處理複雜的狀態或邏輯。當與其它 Hooks(useContext)結合使用,有時也是一個好的選擇,不需要引入一些第三方狀態管理庫,例如 Redux、Mobx。

目標

在本文結束時,您將瞭解:

什麼是 Context?

Context 解決了跨組件之間的通信,也是官方默認提供的一個方案,無需引入第三方庫,適用於存儲對組件樹而言是全局的數據狀態,並且不要有頻繁的數據更新。例如:主題、當前認證的用戶、首選語言。

使用 React.createContext 方法創建一個上下文,該方法接收一個參數做爲其默認值,返回 MyContext.Provider、MyContext.Consumer React 組件。

const MyContext = React.createContext(defaultValue);

MyContext.Provider 組件接收 value 屬性用於傳遞給子組件(使用 MyContext.Consumer 消費的組件),無論嵌套多深都可以接收到。

<MyContext.Provider value={color: 'blue'}>
  {children}
</MyContext.Provider>

將我們的內容包裝在 MyContext.Consumer 組件中,以便訂閱 context 的變更,類組件中通常會這樣寫。

<MyContext.Consumer>
  {value => <span>{value}</span>}}
</MyContext.Consumer>

以上引入不必要的代碼嵌套也增加了代碼的複雜性,React Hooks 提供的 useContext 使得訪問上下文狀態變得更簡單。

const App = () ={
  const value = useContext(newContext);
  console.log(value); // this will return { color: 'black' }
  
  return <div></div>
}

以上我們對 Context 做一個簡單瞭解,更多內容參考官網 Context、useContext 文檔描述,下面我們通過兩個例子來學習如何使用 useContext 管理全局狀態。

useState + useContext 主題切換

本節的第一個示例是使用 React hooks 的 useState 和 useContext  API 實現暗黑主題切換。

實現 Context 的 Provider

在 ThemeContext 組件中我們定義主題爲 light、dark。定義 ThemeProvider 在上下文維護兩個屬性:當前選擇的主題 theme、切換主題的函數 toggleTheme()。

通過 useContext hook 可以在其它組件中獲取到 ThemeProvider 維護的兩個屬性,在使用 useContext 時需要確保傳入 React.createContext 創建的對象,在這裏我們可以自定義一個 hook useTheme 便於在其它組件中直接使用。

代碼位置:src/contexts/ThemeContext.js

import React, { useState, useContext } from "react";

export const themes = {
  light: {
    type: 'light',
    background: '#ffffff',
    color: '#000000',
  },
  dark: {
    type: 'dark',
    background: '#000000',
    color: '#ffffff',
  },
};
const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () ={},
});

export const ThemeProvider = ({ children }) ={
  const [theme, setTheme] = useState(themes.dark);
  const context = {
    theme,
    toggleTheme: () => setTheme(theme === themes.dark
      ? themes.light
      : themes.dark)
  }
  return <ThemeContext.Provider value={context}>
    { children }
  </ThemeContext.Provider>
}

export const useTheme = () ={
  const context = useContext(ThemeContext);
  return context;
};

創建一個 AppProviders,用來組裝創建的多個上下文。代碼位置:src/contexts/index.js

import { ThemeProvider } from './ThemeContext';

const AppProviders = ({ children }) ={
  return <ThemeProvider>
    { children }
  </ThemeProvider>
}
export default AppProviders;

實現 ToggleTheme 組件

在 App.js 文件中,將 AppProviders 組件做爲根組件放在最頂層,這樣被包裹的組件都可以使用 AppProviders 組件提供的屬性。

代碼位置:src/App.js

import AppProviders from './contexts';
import ToggleTheme from './components/ToggleTheme';
import './App.css';

const App = () =(
  <AppProviders>
    <ToggleTheme />
  </AppProviders>
);

export default App;

在 ToggleTheme 組件中,我們使用自定義的 useTheme hook 訪問 theme 對象和 toggleTheme 函數,以下創建了一個簡單主題切換,用來設置背景顏色和文字顏色。

代碼位置:src/components/ToggleTheme.jsx

import { useTheme } from '../contexts/ThemeContext'
const ToggleTheme = () ={
  const { theme, toggleTheme } = useTheme();
  return <div style={{
    backgroundColor: theme.background,
    color: theme.color,
    width: '100%',
    height: '100vh',
    textAlign: 'center',
  }}>
    <h2 class> Toggling Light/Dark Theme </h2>
    <p class> Toggling Light/Dark Theme in React with useState and useContext </p>
    <button class onClick={toggleTheme}>
      Switch to { theme.type } mode
    </button>
  </div>
}
export default ToggleTheme;

Demo 演示

視頻詳情

示例代碼地址:https://github.com/qufei1993/react-state-example/tree/usestate-usecontext-theme

useReducer + useContext 實現 Todos

使用 useReducer 和 useContext 完成一個 Todos。這個例子很簡單,可以幫助我們學習如何實現一個簡單的狀態管理工具,類似 Redux 這樣可以跨組件共享數據狀態。

reducer 實現

src/reducers 目錄下實現 reducer 需要的邏輯,定義的 initialState 變量、reducer 函數都是爲 useReducer 這個 Hook 做準備的,在這個地方需要都導出下,reducer 函數是一個純函數,瞭解 Redux 的小夥伴對這個概念應該不陌生。

// src/reducers/todos-reducer.jsx
export const TODO_LIST_ADD = 'TODO_LIST_ADD';
export const TODO_LIST_EDIT = 'TODO_LIST_EDIT';
export const TODO_LIST_REMOVE = 'TODO_LIST_REMOVE';

const randomID = () => Math.floor(Math.random() * 10000);
export const initialState = {
  todos: [{ id: randomID(), content: 'todo list' }],
};

const reducer = (state, action) ={
  switch (action.type) {
    case TODO_LIST_ADD: {
      const newTodo = {
        id: randomID(),
        content: action.payload.content
      };
      return {
        todos: [ ...state.todos, newTodo ],
      }
    }
    case TODO_LIST_EDIT: {
      return {
        todos: state.todos.map(item ={
          const newTodo = { ...item };
          if (item.id === action.payload.id) {
            newTodo.content = action.payload.content;
          }
          return newTodo;
        })
      }
    }
    case TODO_LIST_REMOVE: {
      return {
        todos: state.todos.filter(item => item.id !== action.payload.id),
      }
    }
    default: return state;
  }
}

export default reducer;

Context 跨組件數據共享

定義 TodoContext 導出 statedispatch,結合 useContext 自定義一個 useTodo hook 獲取信息。

// src/contexts/TodoContext.js
import React, { useReducer, useContext } from "react";
import reducer, { initialState } from "../reducers/todos-reducer";

const TodoContext = React.createContext(null);

export const TodoProvider = ({ children }) ={
  const [state, dispatch] = useReducer(reducer, initialState);
  const context = {
    state,
    dispatch
  }
  return <TodoContext.Provider value={context}>
    { children }
  </TodoContext.Provider>
}

export const useTodo = () ={
  const context = useContext(TodoContext);
  return context;
};
// src/contexts/index.js
import { TodoProvider } from './TodoContext';

const AppProviders = ({ children }) ={
  return <TodoProvider>
    { children }
  </TodoProvider>
}

export default AppProviders;

實現 Todos 組件

在 TodoAdd、Todo、Todos 三個組件內分別都可以通過 useTodo() hook 獲取到 state、dispatch。

import { useState } from "react";
import { useTodo } from "../../contexts/TodoContext";
import { TODO_LIST_ADD, TODO_LIST_EDIT, TODO_LIST_REMOVE } from "../../reducers/todos-reducer";

const TodoAdd = () ={
  console.log('TodoAdd render');
  const [content, setContent] = useState('');
  const { dispatch } = useTodo();

  return <div class>
    <input class onChange={e => setContent(e.target.value)} />
    <button class onClick={() ={
      dispatch({ type: TODO_LIST_ADD, payload: { content } })
    }}>
      添加
    </button>
  </div>
};

const Todo = ({ todo }) ={
  console.log('Todo render');
  const { dispatch } = useTodo();
  const [isEdit, setIsEdit] = useState(false);
  const [content, setContent] = useState(todo.content);

  return <div class>
    {
      !isEdit ? <>
        <div class>{todo.content}</div>
        <button class onClick={() => setIsEdit(true)}> 編輯 </button>
        <button class onClick={() => dispatch({ type: TODO_LIST_REMOVE, payload: { id: todo.id } })}> 刪除 </button>
      </> : <>
        <div class>
          <input class value={content} type="text" onChange={ e => setContent(e.target.value) } />
        </div>
        <button class onClick={() ={
          setIsEdit(false);
          dispatch({ type: TODO_LIST_EDIT, payload: { id: todo.id, content } })
        }}> 更新 </button>
        <button class onClick={() => setIsEdit(false)}> 取消 </button>
      </>
    }
  </div>
}

const Todos = () ={
  console.log('Todos render');
  const { state } = useTodo();
  
  return <div class>
    <h2 class> Todos App </h2>
    <p class> useReducer + useContent 實現 todos </p>
    <TodoAdd />
    <div class>
      {
        state.todos.map(todo => <Todo key={todo.id} todo={todo} />)
      }
    </div>
  </div>
}

export default Todos;

Demo 演示

上面代碼實現需求是沒問題,但是存在一個性能問題,如果 Context 中的某個熟悉發生變化,所有依賴該 Context 的組件也會被重新渲染,觀看以下視頻演示:

視頻詳情

示例代碼地址:https://github.com/qufei1993/react-state-example/tree/usereducer-usecontext-todos

Context 小結

useState/useReducer 管理的是組件的狀態,如果子組件想獲取根組件的狀態一種簡單的做法是通過 Props 層層傳遞,另外一種是把需要傳遞的數據封裝進 Context 的 Provider 中,子組件通過 useContext 獲取來實現全局狀態共享。

Context 對於構建小型應用程序時,相較於 Redux,實現起來會更容易且不需要依賴第三方庫,同時還要看下適用場景。在官網也有說明,適用於存儲對組件樹而言是全局的數據狀態,並且不要有頻繁的數據更新(例如:主題、當前認證的用戶、首選語言)。

以下是使用 Context 會遇到的幾個問題:

在我們實際的 React 項目中沒有一個 Hook 或 API 能解決我們所有的問題,根據應用程序的大小和架構來選擇適合於您的方法是最重要的

Reference

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/pW-watZsmAtjjCPbHANOQw