React Context—跨组件通信

跨组件通信是一个老生常谈的话题,或许你也遇到这样的场景:在多个组件中使用某个状态,并且可能会跨组件改变它。

方案比较

一种方案是状态提升:将状态和操作提升到父组件中,然后通过 props 向下传递。在某些情况下确实能够完美解决问题,代码如下:

function Counter({ count, onIncrementClick }) {
  return <button onClick={onIncrementClick}>{count}</button>; // 操作行为
}

function CountDisplay({ count }) {
  return <div>The current counter count is {count}</div>; // 状态
}

function App() {
  const [count, setCount] = React.useState(0); // 将状态提升到父组件中,通过 props 传递
  const increment = () => setCount((c) => c + 1);

  return (
    <div>
      <CountDisplay count={count} />
      <Counter count={count} onIncrementClick={increment} />
    </div>
  );
}
复制代码

但是上面这种方案也有一些限制:

  • 随着子组件数量上升,父组件会包含大量状态管理的代码
  • 在多层嵌套组件中会导致 prop drilling(参数逐级透传),由于属性的层层传递,将使组件变的复杂并难以维护

下面这段代码,每一层组件都会获得 theme 这个属性,即使当前组件根本不会用到它。这是一种基本操作,但你维护起来应该会非常恼火:

function App() {
	return <Toolbar theme="dark" />;
}

function Toolbar(props) {
  // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
  // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
  // 因为必须将这个值层层传递所有组件。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

function ThemedButton(props) {
  render() {
    return <Button theme={props.theme} />;
  }
}
复制代码

当然,你也可以选择 Redux 全局状态管理库,但是我个人更倾向于对全局状态,如:用户信息、当前主题……才用 Rudux 来管理。无脑的使用 Redux,会带来很大的性能成本。

我这里想介绍的方案是使用 context 来实现状态的跨组件共享,并结合 UseReducer 轻松的实现状态修改。可以叫做”局部状态管理“,但是侵入性没有 Rudex 强,也没有过多的模版代码。

React 官网有关于 context 使用文档,但是我觉得案例比较基础(或者说苍白无力?),进阶的用法应该是什么样的呢?

这里有一个计数器的例子,可以手动增加、减少和重置数值。

获得 context

App.tsx 包含了 CountPlay(展示数值),CountButton(控制增加、减少和重置操作)

// App.tsx

import * as React from 'react';
import { CountProvider, CountContext } from './countContext';

const CountPlay = () => {
  const context = React.useContext(CountContext);
  return <div>{context?.count}</div>;
};

const CountButton = () => {
  return (
    <>
      <button>+ 1</button>
      <button>- 1</button>
      <button>reset</button>
    </>
  );
};

export default function App() {
  return (
    <div className="App">
      <CountProvider>
        <CountPlay></CountPlay>
        <CountButton></CountButton>
      </CountProvider>
    </div>
  );
}
复制代码

countContext.tsx 文件中创建了 CountContext 对象,设置 count 初始值为 0。最后导出

  • CountProvider 组件,被它包裹的子组件才可以获得 context 数据
  • CountContext 对象,定义了 context 元数据
// countContext.tsx

import * as React from 'react';

type CountProviderProps = {
  children: React.ReactNode;
};

// 创建 Context 对象
const CountContext = React.createContext<{ count: number } | undefined>(undefined);

const CountProvider = ({ children }: CountProviderProps) => {
  return <CountContext.Provider value={{ count: 0 }}>{children}</CountContext.Provider>;
};

export { CountProvider, CountContext };
复制代码

改变数值

使用 useReducer 获得动态 state 与静态 dispatch,state 包含了数值 count,dispatch 可以根据 action.type 修改 count 的值。最后导出 context ,共享 state 与 dispatch

// countContext.tsx

import * as React from 'react';

type CountProviderProps = {
  children: React.ReactNode;
};
type State = {
  count: number;
};
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' };

// 创建 Context 对象
const CountContext = React.createContext<{ state: State } | undefined>(undefined);

// reducer 控制数据改变
const countReducer = (state: State, action: Action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error('Unhandle action type of counReducer');
  }
};

const CountProvider = ({ children }: CountProviderProps) => {
  const [state, dispatch] = React.useReducer(countReducer, { count: 0 });
  // 在 context 中共享 state 与 dispatch
  const value = { state, dispatch };
  return <CountContext.Provider value={value}>{children}</CountContext.Provider>;
};

export { CountProvider, CountContext };
复制代码
// App.tsx

import * as React from "react";
import { CountProvider, CountContext } from "./countContext";

const CountPlay = () => {
	// 使用 state
  const { state } = React.useContext(CountContext);
  return <div> {state?.count}</div>;
};

const CountButton = () => {
	// 使用 dispatch 修改 count 的值,实现跨组件状态变更
  const { dispatch } = React.useContext(CountContext);
  return (
    <>
      <button onClick={() => dispatch({ type: "increment" })}>+ 1</button>
      <button onClick={() => dispatch({ type: "decrement" })}>- 1</button>
      <button onClick={() => dispatch({ type: "reset" })}>reset</button>
    </>
  );
};

...
复制代码

优化

state 和 dispatch 有 TS 报错,因为 React.createContext 的初始值值为 undefined,而 undefined 是不能解构的。

截屏2022-03-16 下午11.15.19.png

我没有使用默认值,而是新加了一个 hooks —— useCount,如果是 undefined 就抛出一个错误,这样大家在使用 useCount 也就不需要判断 undefined 了。

// countContext.tsx

...

const useCount = () => {
  const context = React.useContext(CountContext);
  if (context === undefined)
    throw new Error("useCount must be used within a CountProvider");
  return context;
};

export { CountProvider, useCount }
复制代码
// App.tsx
const CountPlay = () => {
  const { state } = useCount();
  return <div> {state.count}</div>;
};

const CountButton = () => {
  const { dispatch } = useCount();
  ...
};
复制代码

总结

随着 useReducer 和 context 能力的增强,使我们能够更容易处理跨组件状态。注意,我不推荐将 context 作为首选方案,因为可以优先考虑状态提升组件组合等方案,究竟该用哪种方式来管理数据,需要视情况而定。

完整代码示例,参考 codeSandbox

Guess you like

Origin juejin.im/post/7075959847061028872