Cross-component communication is an old topic, and you may have encountered situations where a state is used in multiple components and may change it across components.

Scheme comparison

One option is state promotion: state and operations are promoted to the parent component and then passed down through props. In some cases it does work perfectly, as follows:

function Counter({ count, onIncrementClick }) {
  return <button onClick={onIncrementClick}>{count}</button>; // Operation behavior
}

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

function App() {
  const [count, setCount] = React.useState(0); // Promote the state to the parent component, passed through props
  const increment = () = > setCount((c) = > c + 1);

  return (
    <div>
      <CountDisplay count={count} />
      <Counter count={count} onIncrementClick={increment} />
    </div>
  );
}
Copy the code

But there are some limitations:

  • As the number of child components increases, the parent component contains a lot of state management code
  • The multi-layer nested components will lead to prop drilling, which will make the components complex and difficult to maintain due to the layer by layer transfer of attributes

In the following code, each layer of the component gets the theme property, even if the current component doesn’t use it at all. This is a basic operation, but it should be very annoying to maintain:

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

function Toolbar(props) {
  // The Toolbar component accepts an additional "theme" property, which is passed to the ThemedButton component.
  // If every single button in your application needs to know the theme value, this would be a hassle,
  // Because this value must be passed layer by layer to all components.
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

function ThemedButton(props) {
  render() {
    return <Button theme={props.theme} />; }}Copy the code

Of course, you can also choose the Redux global state management library, but I personally prefer global state, such as: user information, current topic… Rudux is used to manage. Mindless use of Redux has a significant performance cost.

The solution I want to introduce here is to use context to share state across components and to easily implement state modification with UseReducer. It could be called “local state management,” but not as intrusive as Rudex and not as much template code.

The React website has documentation on how to use context, but I find the example fairly basic (or weak?). What should the advanced usage be?

Here is an example of a counter that can be manually increased, decreased, and reset.

To get the context

App.tsx includes CountPlay (displaying values), CountButton (adding, subtracting and resetting operations)

// 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>
  );
}
Copy the code

The countContext object is created in the countContext.tsx file and the initial value of count is set to 0. Finally, the export

  • The CountProvider component, which is wrapped around the child components can get the context data
  • CountContext object, which defines the context metadata
// countContext.tsx

import * as React from 'react';

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

// Create the Context object
const CountContext = React.createContext<{ count: number } | undefined> (undefined);

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

export { CountProvider, CountContext };
Copy the code

Change the value

Use useReducer to obtain dynamic state and static dispatch. State contains the value count. Dispatch can change the value of count according to action.type. Finally, export the context and share state and 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' };

// Create the Context object
const CountContext = React.createContext<{ state: State } | undefined> (undefined);

// Reduce control data changes
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 });
  // Share state and dispatch in context
  const value = { state, dispatch };
  return <CountContext.Provider value={value}>{children}</CountContext.Provider>;
};

export { CountProvider, CountContext };
Copy the code
// App.tsx

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

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

const CountButton = () = > {
	// Use dispatch to change the value of count across components
  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>
    </>); }; .Copy the code

To optimize the

State and dispatch have TS errors because the initial value of React. CreateContext is undefined, and undefined is undestructible.

Instead of using the default, I added an hooks — useCount, which raises an error if undefined, so you don’t need to judge undefined when using useCount.

// 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 }
Copy the code
// App.tsx
const CountPlay = () = > {
  const { state } = useCount();
  return <div> {state.count}</div>;
};

const CountButton = () = > {
  const{ dispatch } = useCount(); . };Copy the code

conclusion

The increased capabilities of useReducer and Context make it easier to handle cross-component state. Note that I do not recommend context as a preferred solution, as state promotion, component composition, etc., can be prioritized, and how you manage data depends on the situation.

For a complete code example, see codeSandbox