开头

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

从最早的类组件使用 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 官网介绍

  1. // Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
  2. // 为当前的 theme 创建一个 context(“light”为默认值)。
  3. const ThemeContext = React.createContext('light');
  4. class App extends React.Component {
  5. render() {
  6. // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
  7. // 无论多深,任何组件都能读取这个值。
  8. // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
  9. return (
  10. <ThemeContext.Provider value="dark">
  11. <Toolbar />
  12. </ThemeContext.Provider>
  13. );
  14. }
  15. }
  16. // 中间的组件再也不必指明往下传递 theme 了。
  17. function Toolbar(props) {
  18. return (
  19. <div>
  20. <ThemedButton />
  21. </div>
  22. );
  23. }
  24. class ThemedButton extends React.Component {
  25. // 指定 contextType 读取当前的 theme context。
  26. // React 会往上找到最近的 theme Provider,然后使用它的值。
  27. // 在这个例子中,当前的 theme 值为 “dark”。
  28. static contextType = ThemeContext;
  29. render() {
  30. return <Button theme={this.context} />;
  31. }
  32. }

createContext 实现跨组件通信的大致过程

  1. const MyContext = React.createContext(defaultValue);
  2. <MyContext.Provider value={/* 某个值 */}>
  3. <App>
  4. ... 多层组件嵌套内有一个 Goods 组件
  5. <Goods />
  6. </App>
  7. </MyContext.Provider >
  8. // 某个 子子子子子组件 Goods
  9. <MyContext.Consumer>
  10. {value => /* 基于 context 值进行渲染*/}
  11. </MyContext.Consumer>

具体实际案例

app.js

  1. import {ThemeContext, themes} from './theme-context';
  2. import ThemeTogglerButton from './theme-toggler-button';
  3. class App extends React.Component {
  4. constructor(props) {
  5. super(props);
  6. this.toggleTheme = () => {
  7. this.setState(state => ({
  8. theme:
  9. state.theme === themes.dark
  10. ? themes.light
  11. : themes.dark,
  12. }));
  13. };
  14. // State 也包含了更新函数,因此它会被传递进 context provider。
  15. this.state = {
  16. theme: themes.light,
  17. toggleTheme: this.toggleTheme,
  18. };
  19. }
  20. render() {
  21. // 整个 state 都被传递进 provider
  22. return (
  23. <ThemeContext.Provider value={this.state}>
  24. <Content />
  25. </ThemeContext.Provider>
  26. );
  27. }
  28. }
  29. function Content() {
  30. return (
  31. <div>
  32. <ThemeTogglerButton />
  33. </div>
  34. );
  35. }
  36. ReactDOM.render(<App />, document.root);
  1. // Theme context,默认的 theme 是 “light” 值
  2. const ThemeContext = React.createContext('light');
  3. // 用户登录 context
  4. const UserContext = React.createContext({
  5. name: 'Guest',
  6. });
  7. class App extends React.Component {
  8. render() {
  9. const {signedInUser, theme} = this.props;
  10. // 提供初始 context 值的 App 组件
  11. return (
  12. <ThemeContext.Provider value={theme}>
  13. <UserContext.Provider value={signedInUser}>
  14. <Layout />
  15. </UserContext.Provider>
  16. </ThemeContext.Provider>
  17. );
  18. }
  19. }
  20. function Layout() {
  21. return (
  22. <div>
  23. <Sidebar />
  24. <Content />
  25. </div>
  26. );
  27. }
  28. // 一个组件可能会消费多个 context
  29. function Content() {
  30. return (
  31. <ThemeContext.Consumer>
  32. {theme => (
  33. <UserContext.Consumer>
  34. {user => (
  35. <ProfilePage user={user} theme={theme} />
  36. )}
  37. </UserContext.Consumer>
  38. )}
  39. </ThemeContext.Consumer>
  40. );
  41. }

封装自己的跨组件管理方式

./connect.js 文件

封装 connect 方法

使用 connect 也是基于 react-redux 思想,把它封装为一个方法。调用 connect 方法返回的是一个高阶组件。并且 connect 方法中支持传入一个函数,来过滤,筛选子组件需要的状态,也便于维护 重新 render 等

  1. import React, { createContext } from 'react';
  2. import { useImmer } from 'use-immer';
  3. // useImmer 文章末尾有介绍推荐
  4. const ctx = createContext();
  5. const { Consumer, Provider } = ctx
  6. const useModel = (initialState) => {
  7. const [state, setState] = useImmer(initialState);
  8. return [
  9. state,
  10. setState
  11. ];
  12. }
  13. const createProvider = () => {
  14. function WrapProvider(props) {
  15. const { children, value } = props;
  16. const [_state, _dispatch] = useModel(value)
  17. return (
  18. <Provider value={{
  19. _state,
  20. _dispatch,
  21. }}>
  22. {children}
  23. </Provider>
  24. )
  25. }
  26. return WrapProvider
  27. }
  28. export const connect = fn => ComponentUi => () => {
  29. return (
  30. <Consumer>
  31. {
  32. state => {
  33. const {_state, _dispatch} = state
  34. const selectState = typeof fn === 'function' ? fn(_state) : _state;
  35. return <ComponentUi _state={selectState} _dispatch={_dispatch} />
  36. }
  37. }
  38. </Consumer>
  39. )
  40. };
  41. export default createProvider;

使用方式

  1. import React from 'react';
  2. import Header from './layout/Header.jsx';
  3. import Footer from './layout/Footer.jsx';
  4. import createProvider from './connect';
  5. const Provider = createProvider()
  6. const initValue = { user: 'xiaoming', age: 12 }
  7. function App() {
  8. return (
  9. <Provider value={initValue}>
  10. <Header />
  11. <Footer />
  12. </Provider>
  13. )
  14. }
  15. export default App;

Header.jsx

  1. import React from 'react';
  2. import { Select } from 'antd';
  3. import { connect } from '../connect';
  4. const { Option } = Select;
  5. function Head({ _state: { user, age }, _dispatch }) {
  6. return (
  7. <div className="logo" >
  8. <Select defaultValue={user} value={user} onChange={(value) => {
  9. _dispatch(draft => {
  10. draft.user = value
  11. })
  12. }}>
  13. <Option value='xiaoming'>小明</Option>
  14. <Option value='xiaohong'>小红</Option>
  15. </Select>
  16. <span>年龄{age}</span>
  17. </div>
  18. )
  19. }
  20. export default connect()(Head);

Footer.jsx

  1. import React, { Fragment } from 'react';
  2. import { Select } from 'antd';
  3. import { connect } from '../../connect';
  4. const { Option } = Select;
  5. function Footer({ _state, _dispatch }) {
  6. const { user, age } = _state;
  7. return (
  8. <Fragment>
  9. <p style={{marginTop: 40}}>用户:{user}</p>
  10. <p>年龄{age}</p>
  11. <div>
  12. <span>改变用户:</span>
  13. <Select
  14. defaultValue={user}
  15. value={user}
  16. onChange={(value) => {
  17. _dispatch(draft => {
  18. draft.user = value
  19. })
  20. }}>
  21. <Option value='xiaoming'>小明</Option>
  22. <Option value='xiaohong'>小红</Option>
  23. </Select></div>
  24. <div>
  25. <span>改变年龄:</span>
  26. <input onChange={(e) => {
  27. // 这里使用 persist 原因可以看文章末尾推荐
  28. e.persist();
  29. _dispatch(draft => {
  30. draft.age = e.target.value
  31. })
  32. }} />
  33. </div>
  34. </Fragment>
  35. )
  36. }
  37. export default connect()(Footer);

使用 useContext

我们都知道 react 16.8 以后也出了 useContext 那么我们可以通过使用 useContext 来优化 connect 方法

  1. // 未使用 useContext
  2. export const connect = (fn) => (ComponentUi) => () => {
  3. const state = useContext(ctx)
  4. console.log(state);
  5. return (
  6. <Consumer>
  7. {
  8. state => {
  9. const { _state, _dispatch } = state
  10. const selectState = typeof fn === 'function' ? fn(_state) : _state;
  11. return <ComponentUi _state={selectState} _dispatch={_dispatch} />
  12. }
  13. }
  14. </Consumer>
  15. )
  16. };
  17. // 使用 useContext
  18. export const connect = fn => ComponentUi => () => {
  19. const { _state, _dispatch } = useContext(ctx);
  20. const selectState = typeof fn === 'function' ? fn(_state) : _state;
  21. return <ComponentUi _state={selectState} _dispatch={_dispatch} />;
  22. };

注意: 调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以通过文章末尾推荐的不必要重新 render 开销大的组件去了解如何优化。

最后

github地址

4步代码跑起来

  1. git clone https://github.com/zouxiaomingya/blog
  2. cd blog
  3. npm i
  4. npm start

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

参考: