Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何,何时使用 useContext #10

Open
zouxiaomingya opened this issue Nov 27, 2019 · 0 comments
Open

如何,何时使用 useContext #10

zouxiaomingya opened this issue Nov 27, 2019 · 0 comments

Comments

@zouxiaomingya
Copy link
Owner

zouxiaomingya commented Nov 27, 2019

开头

一个项目,一个复杂的逻辑,我觉得状态管理显得尤为的重要,状态管理的好不好,直接体现了一个项目的逻辑性、可读性、维护性等是否清晰,易读,和高效。

从最早的类组件使用 this.state, this.setState 去管理状态,到 redux , subscribe, dispatch 的发布订阅,redux 的使用就面临重复和沉重的的 reducer,让我俨然变成了 Ctrl CV 工程师。于是后面接触 dva,它是一个基于 reduxredux-saga 的数据流方案。通过 model 来分片管理全局状态,使用 connect 方法去给需要的深层次的组件传递状态。

到后面 react hooks 出来之后,业界也出了很多自身管理状态的,基于 hooks 封装,各个模块都有一个基于自己 hooks 的状态 store。确实很好的解决了函数组件的状态管理,和模块自身内部的状态管理,但是还是解决不了在全局组件中,层层传递的状态依赖让结构变得复杂,繁琐的问题。不用任何的管理工具我们如何做到跨组件通信?

为什么不用?

不是说我们不去用 dva 这样的管理工具?我并不是说 dva 不好用,而是我觉得有时候没必要用。我觉得他太重了。

学到什么?

读完这边文章,即使你觉得我的管理方式不好,你也可以学习和了解到 useMemo, useContext,useImmer等。

react context

Context-React 官网介绍

// 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.js

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 文件

封装 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.jsx

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

全文章,如有错误或不严谨的地方,请务必给予指正,谢谢!

参考:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant