基於React Context實現一個簡單的狀態管理的示例代碼

前言

在大多數情況下,我們開發項目都需要一個狀態管理,方便我們在全局共享狀態庫,在React生態裡比較流行的幾個庫

redux、mobx、recoil

但是對於小項目,我們完全可以自己封裝一個狀態管理,減少一個包的安裝就可以減小打包以後的項目體積。 主要分兩步:

  • 封裝一個頂層組件提供數據
  • 子組件獲取數據和更新數據

封裝一個父組件用來包裹其他子組件

stores/index.js 文件中首先需要調用 createContext

export const MyContext = React.createContext({list: [], data: null, time: Date.now()});

createContext 中的參數是默認值,隻有當組件所處的樹中沒有匹配到 Provider 時,其參數才會生效。

每個 Context 對象都會返回一個 Provider React 組件,它允許消費組件訂閱 context 的變化。

創建一個 Context 對象。當 React 渲染一個訂閱瞭這個 Context 對象的組件,這個組件會從組件樹中匹配離自身最近的Provider,並從中讀取到當前的 context 值。

context 可以設置一個displayName 的屬性, 可以方便在React DevTool 對該context調試。

MyContext.displayName = 'MyManagementDisplayName';

Provider 接收一個 value 屬性,傳遞給消費組件。 Context 能讓你將這些數據向組件樹下所有的組件進行“廣播”,所有的組件都能訪問到這些數據,也能訪問到後續的數據更新。

這裡我們封裝一個父組件用來包裹其他子組件。

import { createContext, useReducer } from 'react';

// 純函數reducer
function reducer(state, action) {
    // action包括 具體的類型type,
    // 除瞭 `type` 之外,action 對象的結構其實完全取決於你自己。
    // 這裡使用瞭payload代表dipatch傳過來的數據
    switch(action.type) {
        case 'list':
            return ({...state, list: action.payload});
        case 'data':
            return ({...state, data: action.payload});
        case 'time':
            return ({...state, time: action.payload});
        default:
            return state;
    }
}
const list = [{num: 0, key: 0}, {num: 1, key: 1}, {num: 2, key: 2}];
export const MyContext = createContext({list: [], data: null, time: Date.now()});

function ContextProvider({children}) {
    const [state, dispatch] = useReducer(
        reducer,
        {list: list, data: null, time: Date.now()}
    );

    const value = {
        state,
        dispatch
    }
    return <MyContext.Provider value={value}>
        {children}
    </MyContext.Provider>
}

export default ContextProvider;

這裡用到瞭useReducer, 用過redux的同學一定非常熟悉,這是因為redux的作者 dan abramov 加入瞭react開發團隊, 是react的主要開發者。 第一個參數是一個處理數據的純函數,第二個參數是 initialValue。 useReducer還有另一種用法可以接受函數作為第三個參數,可以惰性地創建初始 state,這不是本文的重點,感興趣的同學可以自行查詢文檔學習。

在入口文件index.js中 用 ContextProvider 包裹 App 組件

import ReactDOM from 'react-dom';
import App from './App';
import './styles/index.less';

import ContextProvider from './stores';
ReactDOM.render(
    <ContextProvider><App /></ContextProvider>,
    document.getElementById('root')
);

子組件如何獲取數據呢

有3種方式

  • Class Component 內獲取(本方法僅能訂閱 1 個 context)
  • context.Consumer
  • useContext

class Component 方式

import {MyContext} from '@/store';

class MyClass extends React.Component {
  static contextType = MyContext;
  // 引入的MyContext 賦值給靜態屬性 contextType後,
  // React可以讓你使用 `this.context` 來獲取最近 Context 上的值。
  componentDidMount() {
    let value = this.context;
    /* 在組件掛載完成後,使用 MyContext 組件的值來執行一些有副作用的操作 */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* 基於 MyContext 組件的值進行渲染 */
  }
}

context.Consumer

<context.Consumer>
  {value => /* 基於 context 值進行渲染* /}
</context.Consumer>

useContext

這是使用 hook 方式, 也是目前最流行的用法,後面的例子主要使用這個方式來演示。 因為我們要在很多需要全局狀態的子組件使用,所以我們可以封裝一下。

在 hooks/useStores.js

import {MyContext} from '@/stores';
import React from 'react';

// 封裝代碼以復用
const useStores = () => React.useContext(MyContext);

export default useStores;

下面我們通過兩個組件,分別演示 獲取數據並展示更新全局數據

views/footer/index.js
在此組件裡獲取全局數據並展示

import { useEffect } from 'react';
import useStores from '../../hooks/useStores';

function Footer() {
  const { state } = useStores();
  const { time, list } = state;
  useEffect(() => {
    console.log('Footer page rendered!!!')
  })
  return (
    <div style={{ height: 200 }}>
      <div>time now is {time}</div>
      <div>
        list is
        {list.map((item) => (
          <span 
              style={{
                  background: 'pink',
                  padding: '0 10px',
                  border: '1px solid',
                  marginRight: '10px'
              }}
              key={item.key}
          >
              {item.num}
          </span>
        ))}
      </div>
    </div>
  );
}

export default Footer;

views/header/index.js

我們在此組件裡更新全局數據

import useStores from '../../hooks/useStores';
import { Link } from 'react-router-dom';
import { useEffect } from 'react';

function Header() {
  // 解構獲取 dispatch 方法
  const { dispatch } = useStores();
  const handleList = () => {
    const payload = [...new Array(3)].map(() => {
      const key = Math.random();
      const num = Math.floor(key * 100);
      return ({
        key, num
      });
    })
    // 更新數據,訂閱狀態的組件都會獲取更新通知並取到最新數據
    dispatch({ type: "list", payload });
  };
  return (
    <div style={{ height: 100 }}>
      <button onClick={() => dispatch({ type: 'time', payload: Date.now() })}>
        time
      </button>
      <button onClick={handleList}>list</button>
    </div>
  );
}

export default Header;

點擊 header 中的按鈕,footer 裡的 time list 都會響應改變,獲取到最新的值並渲染展示。

總結

我們通過封裝頂層組件提供全局數據,子組件獲取和更新數據, 完全基於 React 實現瞭一個簡單的狀態管理。

當然 Context 是可以嵌套多層的,同學們可以自行嘗試

參考

  • React Context
  • React useReducer

 到此這篇關於基於React Context實現一個簡單的狀態管理的示例代碼的文章就介紹到這瞭,更多相關React Context狀態管理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: