开头
一个项目,一个复杂的逻辑,我觉得状态管理显得尤为的重要,状态管理的好不好,直接体现了一个项目的逻辑性、可读性、维护性等是否清晰,易读,和高效。
从最早的类组件使用 this.state, this.setState 去管理状态,到 redux , subscribe, dispatch 的发布订阅,redux 的使用就面临重复和沉重的的 reducer,让我俨然变成了 Ctrl CV 工程师。于是后面接触 dva,它是一个基于 redux 和 redux-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 都被传递进 providerreturn (<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');// 用户登录 contextconst 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>);}// 一个组件可能会消费多个 contextfunction 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 } = ctxconst 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} = stateconst 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><SelectdefaultValue={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 方法
// 未使用 useContextexport const connect = (fn) => (ComponentUi) => () => {const state = useContext(ctx)console.log(state);return (<Consumer>{state => {const { _state, _dispatch } = stateconst selectState = typeof fn === 'function' ? fn(_state) : _state;return <ComponentUi _state={selectState} _dispatch={_dispatch} />}}</Consumer>)};// 使用 useContextexport 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 开销大的组件去了解如何优化。
最后
4步代码跑起来
git clone https://github.com/zouxiaomingya/blogcd blognpm inpm start
全文章,如有错误或不严谨的地方,请务必给予指正,谢谢!
参考:
