yiksanchan
yiksanchan

Reputation: 1940

Multiple React Context: inner one depending on outer one doesn't work

I want to use 2 React Context for my app.

  1. The 1st context is UserContext, that loads user info.
  2. The 2nd context is ItemContext, that loads item info based on user id.

For simplicity, I use synchronous functions to load user and item info. See CodeSandbox for a full working sample.

// index.js

import React from "react";
import ReactDOM from "react-dom";
import { ItemProvider, ItemConsumer } from "./ItemContext";
import { UserProvider, UserConsumer } from "./UserContext";

class App extends React.Component {
  render() {
    return (
      <UserProvider>
        <UserConsumer>
          {({ user }) => (
            <ItemProvider user={user}>
              <ItemConsumer>
                {({ item }) => <div>{JSON.stringify(item, null, 2)}</div>}
              </ItemConsumer>
            </ItemProvider>
          )}
        </UserConsumer>
      </UserProvider>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

// UserContext.js
import React from "react";

const { Provider, Consumer } = React.createContext();

class UserProvider extends React.Component {
  state = {
    isLoading: false,
    value: { user: {} }
  };

  async componentDidMount() {
    this.setState({ isLoading: true });
    const user = { id: 1, name: "evan" };
    this.setState({ value: { user }, isLoading: false });
  }

  render() {
    const { isLoading, value } = this.state;
    if (isLoading) {
      return <div>Loading data...</div>;
    }
    return <Provider value={value}>{this.props.children}</Provider>;
  }
}

export { UserProvider, Consumer as UserConsumer };

// ItemContext.js
import React from "react";

const { Provider, Consumer } = React.createContext();

const items = { 1: { name: "a red hat" }, 2: { name: "a blue shirt" } };

const getItemByUserId = userId => items[userId];

class ItemProvider extends React.Component {
  state = {
    isLoading: false,
    value: { item: {} }
  };

  async componentDidMount() {
    this.setState({ isLoading: true });
    const item = getItemByUserId(1);
    // console.log(this.props.user); // Object {}
    // const item = getItemByUserId(this.props.user.id);
    this.setState({ value: { item }, isLoading: false });
  }

  render() {
    const { isLoading, value } = this.state;
    if (isLoading) {
      return <div>Loading data...</div>;
    }
    return <Provider value={value}>{this.props.children}</Provider>;
  }
}

export { ItemProvider, Consumer as ItemConsumer };

I am not able to do const item = getItemByUserId(this.props.user.id); in ItemContext because this.props.user is an empty object.

What is the solution?

Upvotes: 1

Views: 704

Answers (1)

Shubham Khatri
Shubham Khatri

Reputation: 282160

User value is rendered from within UserContext asynchronously and won't be available until the children have rendered and hence you need to implement componentDidUpdate in ItemProvider to get the context value based on user prop

import React from "react";

const { Provider, Consumer } = React.createContext();

const items = { 1: { name: "a red hat" }, 2: { name: "a blue shirt" } };

const getItemByUserId = userId => items[userId];

class ItemProvider extends React.Component {
  state = {
    isLoading: false,
    value: { item: {} }
  };

  async componentDidMount() {
    if (this.props.user && this.props.user.id) {
      this.setState({ isLoading: true });
      const item = getItemByUserId(this.props.user.id);
      this.setState({ value: { item }, isLoading: true });
    }
  }

  componentDidUpdate(prevProps) {
    console.log(this.props.user, prevProps.user);
    if (prevProps.user !== this.props.user) {
      const item = getItemByUserId(this.props.user.id);
      this.setState({
        value: { item }
      });
    }
  }

  render() {
    const { isLoading, value } = this.state;
    if (isLoading) {
      return <div>Loading data...</div>;
    }
    console.log(value);
    return <Provider value={value}>{this.props.children}</Provider>;
  }
}

export { ItemProvider, Consumer as ItemConsumer };

Working demo

Upvotes: 1

Related Questions