使用 useContext 封裝本身的狀態管理(十幾行代碼)

開頭

一個項目,一個複雜的邏輯,我以爲狀態管理顯得尤其的重要,狀態管理的好很差,直接體現了一個項目的邏輯性、可讀性、維護性等是否清晰,易讀,和高效。javascript

從最先的類組件使用 this.state, this.setState 去管理狀態,到 redux , subscribe, dispatch 的發佈訂閱,redux 的使用就面臨重複和沉重的的 reducer,讓我儼然變成了 Ctrl CV 工程師。因而後面接觸 dva,它是一個基於 reduxredux-saga 的數據流方案。經過 model 來分片管理全局狀態,使用 connect 方法去給須要的深層次的組件傳遞狀態。html

到後面 react hooks 出來以後,業界也出了不少自身管理狀態的,基於 hooks 封裝,各個模塊都有一個基於本身 hooks 的狀態 store。確實很好的解決了函數組件的狀態管理,和模塊自身內部的狀態管理,可是仍是解決不了在全局組件中,層層傳遞的狀態依賴讓結構變得複雜,繁瑣的問題。不用任何的管理工具咱們如何作到跨組件通訊?java

爲何不用?

不是說咱們不去用 dva 這樣的管理工具?我並非說 dva 很差用,而是我以爲有時候不必用。我以爲他過重了。react

學到什麼?

讀完這邊文章,即便你以爲個人管理方式很差,你也能夠學習和了解到 useMemo, useContext,useImmer等。git

react context

Context-React 官網介紹github

// Context 可讓咱們無須明確地傳遍每個組件,就能將值深刻傳遞進組件樹。
// 爲當前的 theme 建立一個 context(「light」爲默認值)。
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // 使用一個 Provider 來將當前的 theme 傳遞給如下的組件樹。
    // 不管多深,任何組件都能讀取這個值。
    // 在這個例子中,咱們將 「dark」 做爲當前的值傳遞下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中間的組件不再必指明往下傳遞 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 讀取當前的 theme context。
  // React 會往上找到最近的 theme Provider,而後使用它的值。
  // 在這個例子中,當前的 theme 值爲 「dark」。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}
複製代碼

createContext 實現跨組件通訊的大體過程

const MyContext = React.createContext(defaultValue);

<MyContext.Provider value={/* 某個值 */}>
    <App>
      ... 多層組件嵌套內有一個 Goods 組件
            <Goods />
  </App>  
</MyContext.Provider >


// 某個 子子子子子組件 Goods
<MyContext.Consumer>
  {value => /* 基於 context 值進行渲染*/}
</MyContext.Consumer>
複製代碼

具體實際案例

app.jsnpm

import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };

    // State 也包含了更新函數,所以它會被傳遞進 context provider。
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    // 整個 state 都被傳遞進 provider
    return (
      <ThemeContext.Provider value={this.state}> <Content /> </ThemeContext.Provider> ); } } function Content() { return ( <div> <ThemeTogglerButton /> </div> ); } ReactDOM.render(<App />, document.root); 複製代碼
// Theme context,默認的 theme 是 「light」 值
const ThemeContext = React.createContext('light');

// 用戶登陸 context
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    // 提供初始 context 值的 App 組件
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}

// 一個組件可能會消費多個 context
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}
複製代碼

封裝本身的跨組件管理方式

./connect.js 文件redux

封裝 connect 方法

使用 connect 也是基於 react-redux 思想,把它封裝爲一個方法。調用 connect 方法返回的是一個高階組件。而且 connect 方法中支持傳入一個函數,來過濾,篩選子組件須要的狀態,也便於維護 從新 render 等數組

import React, { createContext } from 'react';
import { useImmer } from 'use-immer';
// useImmer 文章末尾有介紹推薦

const ctx = createContext();
const { Consumer, Provider } = ctx

const useModel = (initialState) => {
  const [state, setState] = useImmer(initialState);
  return [
    state,
    setState
  ];
}

const createProvider = () => {
  function WrapProvider(props) {
    const { children, value } = props;
    const [_state, _dispatch] = useModel(value)
    return (
      <Provider value={{ _state, _dispatch, }}> {children} </Provider>
    )
  }
  return WrapProvider
}

export const connect = fn => ComponentUi => () => {
  return (
    <Consumer> { state => { const {_state, _dispatch} = state const selectState = typeof fn === 'function' ? fn(_state) : _state; return <ComponentUi _state={selectState} _dispatch={_dispatch} /> } } </Consumer> ) }; export default createProvider; 複製代碼

使用方式

import React from 'react';
import Header from './layout/Header.jsx';
import Footer from './layout/Footer.jsx';
import createProvider from './connect';

const Provider = createProvider()

const initValue = { user: 'xiaoming', age: 12 }
function App() {
  return (
    <Provider value={initValue}> <Header /> <Footer /> </Provider>

  )
}
export default App;
複製代碼

Header.jsxbash

import React from 'react';
import { Select } from 'antd';
import { connect } from '../connect';
const { Option } = Select;

function Head({ _state: { user, age }, _dispatch }) {
  return (
    <div className="logo" > <Select defaultValue={user} value={user} onChange={(value) => { _dispatch(draft => { draft.user = value }) }}> <Option value='xiaoming'>小明</Option> <Option value='xiaohong'>小紅</Option> </Select> <span>年齡{age}</span> </div>
  )
}

export default connect()(Head);
複製代碼

Footer.jsx

import React, { Fragment } from 'react';
import { Select } from 'antd';
import { connect } from '../../connect';

const { Option } = Select;
function Footer({ _state, _dispatch }) {
  const { user, age } = _state;
  return (
    <Fragment> <p style={{marginTop: 40}}>用戶:{user}</p> <p>年齡{age}</p> <div> <span>改變用戶:</span> <Select defaultValue={user} value={user} onChange={(value) => { _dispatch(draft => { draft.user = value }) }}> <Option value='xiaoming'>小明</Option> <Option value='xiaohong'>小紅</Option> </Select></div> <div> <span>改變年齡:</span> <input onChange={(e) => { // 這裏使用 persist 緣由能夠看文章末尾推薦 e.persist(); _dispatch(draft => { draft.age = e.target.value }) }} /> </div> </Fragment> ) } export default connect()(Footer); 複製代碼

使用 useContext

咱們都知道 react 16.8 之後也出了 useContext 那麼咱們能夠經過使用 useContext 來優化 connect 方法

// 未使用 useContext
export const connect = (fn) => (ComponentUi) => () => {
  const state = useContext(ctx)
  console.log(state);
  return (
    <Consumer>
      {
        state => {
          const { _state, _dispatch } = state
          const selectState = typeof fn === 'function' ? fn(_state) : _state;
          return <ComponentUi _state={selectState} _dispatch={_dispatch} />
        }
      }
    </Consumer>
  )
};

// 使用 useContext
export const connect = fn => ComponentUi => () => {
  const { _state, _dispatch } = useContext(ctx);
  const selectState = typeof fn === 'function' ? fn(_state) : _state;
  return <ComponentUi _state={selectState} _dispatch={_dispatch} />;
};
複製代碼

注意: 調用了 useContext 的組件總會在 context 值變化時從新渲染。若是重渲染組件的開銷較大,你能夠經過文章末尾推薦的沒必要要從新 render 開銷大的組件去了解如何優化。

最後

github地址

4步代碼跑起來

git clone https://github.com/zouxiaomingya/blog
cd blog
npm i
npm start
複製代碼

全文章,若有錯誤或不嚴謹的地方,請務必給予指正,謝謝!

參考:

相關文章
相關標籤/搜索