Home > Mobile >  How do I update state through a React context?
How do I update state through a React context?

Time:05-20

Here's a trivial example using a React context:

type MyState = { counter: number };

const MyContext = createContext<[MyState, () => void]|undefined>(undefined);

class MyComponent extends Component<any, MyState> {
  constructor(props: any) {
    super(props);
    this.state = { counter: 42 };
  }
  render() {
    return (
      <MyContext.Provider value={[this.state, () => this.setState({ counter: this.state.counter   1 })]}>
        {this.props.children}
      </MyContext.Provider>
    )
  }
}

function App() {
  return (
    <MyComponent>
      <MyContext.Consumer>
        {
          pair => {
            const [state, increment] = pair || [];
            return (<button onClick={increment}>Click to increment: {state?.counter}</button>);
          }
        }
      </MyContext.Consumer>
    </MyComponent>
  );
}

This works, and clicking the button increments the counter as expected. But when I try to pull up the incrementer into a method on the parent component:

type MyState = { counter: number };

const MyContext = createContext<MyComponent|undefined>(undefined);

class MyComponent extends Component<any, MyState> {
  constructor(props: any) {
    super(props);
    this.state = { counter: 42 };
  }
  render() {
    return (
      <MyContext.Provider value={this}>
        {this.props.children}
      </MyContext.Provider>
    )
  }
  increment() {
    this.setState({ counter: this.state.counter   1 });
  }
}

function App() {
  return (
    <MyComponent>
      <MyContext.Consumer>
        {
          me => (<button onClick={() => me?.increment()}>Click to increment: {me?.state.counter}</button>)
        }
      </MyContext.Consumer>
    </MyComponent>
  );
}

Now the button no longer repaints when clicked.

The counter is incrementing, but the state change doesn't propagate to the consumer. Why is this?

I suppose it's possible to use a reducer instead of an object:

type MyState = { counter: number };
type MyAction = { type: "increment" };

const MyContext = createContext<[MyState, React.Dispatch<MyAction>]|undefined>(undefined);

function myReducer(state: MyState, action: MyAction) {
  switch (action.type) {
    case "increment": 
      return { counter: state.counter   1 };
    default:
      throw new Error();
  }
}

function MyComponent({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(myReducer, { counter: 42 });
  return (
    <MyContext.Provider value={[state, dispatch]}>
      {children}
    </MyContext.Provider>
  )
}

function App() {
  return (
    <MyComponent>
      <MyContext.Consumer>
        {
          pair => {
            const [state, dispatch] = pair || [];
            return (<button onClick={() => dispatch && dispatch({ type: "increment" })}>Click to increment: {state?.counter}</button>);
          }
        }
      </MyContext.Consumer>
    </MyComponent>
  );
}

But this means cutting up existing code that's nicely-encapsulated in a class into a reducer function with a big switch statement. Is there a way to avoid this?

CodePudding user response:

I think you should not put this as the value of the context on your provider.

When you're passing () => this.setState({ counter: this.state.counter 1 })]} you're passing an anonymous function (() => {}) that runs your this.setState(). As it is an arrow function, it guarantees that this.setState and this.state are refereeing to the correct this.

If you'd like to use the increment method, you can keep using the same logic:

<MyContext.Provider value={[this.state, () => this.increment()]}>

Of course, you don't need to use an array if you want, you can use an object:

<MyContext.Provider value={{ state: this.state, increment: () => this.increment() }}>

This is common in React, and it's better to store in the context only the values you need to pass to the consumers, this way you can better understand which part of your code is changing what, and only have access to what it needs.

  • Related