在 React 應用中使用 Hooks 與 Context 替代 Redux 狀態管理

React Hooks 在 2018 年年底就已經公佈了,正式發佈是在 2019 年 5 月,關於它到底能做什麼用,並不在本文的探討範圍之內,本文旨在摸索,如何基於 Hooks 以及 Context,實現多組件的狀態共享,完成一個精簡版的 Redux。

初始化一個 React 項目

yarn create create-app hooks-context-based-state-management-react-app
cd hooks-context-based-state-management-react-app
yarn start

或者可以直接 clone 本文完成的項目:

git clone https://github.com/pantao/hooks-context-based-state-management-react-app.git

設置我們的 state

絕大多數情況下,我們其實只需要共享會話狀態即可,在本文的示例中,我們也就只共享這個,在 src 目錄下,創建一個 store/types.js 文件,它定義我們的 action 類型:

// 設置 session
const SET_SESSION = "SET_TOKEN";
// 銷燬會話
const DESTROY_SESSION = "DESTROY_SESSION";

export { SET_SESSION, DESTROY_SESSION };

export default { SET_SESSION, DESTROY_SESSION };

接着定義我們的 src/reducers.js

import { SET_SESSION, DESTROY_SESSION } from "./types";

const initialState = {
  // 會話信息
  session: {
    // J.W.T Token
    token: "",
    // 用戶信息
    user: null,
    // 過期時間
    expireTime: null
  }
};

const reducer = (state = initialState, action) => {
  console.log({ oldState: state, ...action });

  const { type, payload } = action;
  switch (type) {
    case SET_SESSION:
      return {
        ...state,
        session: {
          ...state.session,
          ...payload
        }
      };
    case DESTROY_SESSION:
      return {
        ...state,
        session: { ...initialState }
      };
    default:
      throw new Error("Unexpected action");
  }
};

export { initialState, reducer };

創建 src/actions.js

import { SET_SESSION, DESTROY_SESSION } from "./types";

export const useActions = (state, dispatch) => {
  return {
    login: async (username, password) => {
      console.log(`login with ${username} & ${password}`);
      const session = await new Promise(resolve => {
        // 模擬接口請求費事 1 秒
        setTimeout(
          () =>
            resolve({
              token: "J.W.T",
              expireTime: new Date("2030-09-09"),
              user: {
                username,
                password
              }
            }),
          1000
        );
      });

      // dispatch SET_TOKEN
      dispatch({
        type: SET_SESSION,
        payload: session
      });

      return session;
    },
    logout: () => {
      dispatch({
        type: DESTROY_SESSION
      });
    }
  };
};

關鍵時刻,創建 store/StoreContext.js

import React, { createContext, useReducer, useEffect } from "react";
import { reducer, initialState } from "./reducers";
import { useActions } from "./actions";

const StoreContext = createContext(initialState);

function StoreProvider({ children }) {
  // 設置 reducer,得到 `dispatch` 方法以及 `state`
  const [state, dispatch] = useReducer(reducer, initialState);

  // 生成 `actions` 對象
  const actions = useActions(state, dispatch);

  // 打印出新的 `state`
  useEffect(() => {
    console.log({ newState: state });
  }, [state]);

  // 渲染 state, dispatch 以及 actions
  return (
    <StoreContext.Provider value={{ state, dispatch, actions }}>
      {children}
    </StoreContext.Provider>
  );
}

export { StoreContext, StoreProvider };

修改 src/index.js

打開 src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

做如下修改:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { StoreProvider } from "./context/StoreContext"; // 導入 StoreProvider 組件

ReactDOM.render(
  <StoreProvider>
    <App />
  </StoreProvider>,
  document.getElementById("root")
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

src/App.js

內容如下:

import React, { useContext, useState } from "react";
import logo from "./logo.svg";
import "./App.css";

import { StoreContext } from "./store/StoreContext";
import { DESTROY_SESSION } from "./store/types";

function App() {
  const { state, dispatch, actions } = useContext(StoreContext);

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const { user, expireTime } = state.session;

  const login = async () => {
    if (!username) {
      return alert("請輸入用戶名");
    }
    if (!password) {
      return alert("請輸入密碼");
    }
    setLoading(true);
    try {
      await actions.login(username, password);
      setLoading(false);
      alert("登錄成功");
    } catch (error) {
      setLoading(false);
      alert(`登錄失敗:${error.message}`);
    }
  };

  const logout = () => {
    dispatch({
      type: DESTROY_SESSION
    });
  };

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        {loading ? <div className="loading">登錄中……</div> : null}
        {user ? (
          <div className="user">
            <div className="field">用戶名:{user.username}</div>
            <div className="field">過期時間:{`${expireTime}`}</div>
            <div className="button" onClick={actions.logout}>
              使用 actions.logout 退出登錄
            </div>
            <div className="button" onClick={logout}>
              使用 dispatch 退出登錄
            </div>
          </div>
        ) : (
          <div className="form">
            <label className="field">
              用戶名:
              <input
                value={username}
                onChange={e => setUsername(e.target.value)}
              />
            </label>
            <label className="field">
              密碼:
              <input
                value={password}
                onChange={e => setPassword(e.target.value)}
                type="password"
              />
            </label>
            <div className="button" onClick={login}>
              登錄
            </div>
          </div>
        )}
      </header>
    </div>
  );
}

export default App;

總結

整個實現我們使用到了 ReactuseContext 共享上下文關係,這個是關係、useEffect 用來實現 reducerloguseReducer 實現 redux 裏面的 combineReducer 功能,整體上來講,實現還是足夠絕大多數中小型項目使用的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章