React: [Question] Context provider state initialisation.

Created on 10 Apr 2018  Â·  22Comments  Â·  Source: facebook/react

Consider a component wrapping a context provider:

class ValueWrapper extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
      setValue: this.setValue,
    };
  }

  setValue = (value) => {
    this.setState({ value });
  };

  render = () => (
    <Context.Provider value={this.state}>
      {this.props.children}
    </Context.Provider>
  );
}

A consumer then might want to set a default value when it is first mounted. The only way I can see to do this using the new API is to check for an existing value on first render:

class ValueUpdater extends React.Component {
  state = {
    inputValue: 0,
  };

  handleInputChange = e => {
    this.setState({ inputValue: e.target.value });
  };

  render = () => (
    <Context.Consumer>
      {({ value, setValue }) => {
        if (!value) {
          setValue(this.state.inputValue);
        }

        return (
          <div>
            <input type="text" value={value} onChange={handleInputChange}/>
            <button onClick={() => setValue(this.state.inputValue)}>Update value</button>
          </div>
        );
      }}
    </Context.Consumer>
  )
};

But this seems to break the golden rule of updating state in the render method (setValue(this.state.inputValue)), as this would immediately cause a re-render. Ideally I would be able to call the setValue from the context in the componentDidMount method of the ValueUpdater component, but with context as a render prop, that's not possible, as far as I can tell. The docs suggest passing props down to another component:

  render = () => (
    <Context.Consumer>
      {({ value, setValue }) => (
          <ValueUpdaterInput setValue={ setValue } value={ value } />
      )}
    </Context.Consumer>
  )

But if I tried to do the initialisation in the ValueUpdaterInput component's componentDidMount method, it would be called on every render, surely, as ValueUpdaterInput would be re-rendered each time?

Is there a better pattern than this, or am I trying to use context inappropriately?

Question

Most helpful comment

Can you provide more details on the specific scenario you're trying to implement?
More concretely:

  • Why do you hoist this state up when the component "knows" its initial state better than the context provider?
  • What happens if you render two such components (and they both attempt to set the value)? Should one of them "win"?

All 22 comments

Sorry if this is the wrong place for this by the way, I have tried to make the examples above as generic as possible, but also I've not seen anything in the docs regarding this sort of pattern, so I'd be happy to draft some documentation on it if there is anything that needs to be added.

EDIT: answering dan's questions will probably land you in better architecture than my approach which "solves the problem"; this might not be the right problem to solve depending on your actual use case.


When I encountered this problem in my own components, the only reasonable alternative I found was to add a third component.

render () {
  return (
    <Context.Consumer>{({ value, setValue }) => (
      <ConsumerRender value={value} setValue={setValue}/>
    )}</Context.Consumer>
  )
}
class ConsumerRender {
  componentDidMount () {
    if (!this.props.value) {
      this.props.setValue('value')
    }
  }
  render () {
    return this.props.value // or whatever you actually want to render
  }
}

Can you provide more details on the specific scenario you're trying to implement?
More concretely:

  • Why do you hoist this state up when the component "knows" its initial state better than the context provider?
  • What happens if you render two such components (and they both attempt to set the value)? Should one of them "win"?

Great questions, thank you for responding. The component I'm trying to trying to implement is a "tabbed" UI component (image from Google Images just for illustration):

Which I've tried to implement with an API like this:

<TabContainer>
  <Tab label="Home" defaultTab={ true }>
    <div>Welcome to the homepage</div>
  </Tab>
  <Tab label="About">
    <div>Welcome to the about page</div>
  </Tab>
  <Tab label="Contact">
    <div>Welcome to the contact page</div>
  </Tab>
  <div>
    <TabContent />
  </div>
</TabContainer>

Where the content rendered into the TabContent component is the child of the related Tab. The idea being that the "default" tab would be able to communicate that it is the default to its parent context. I suppose an alternative API for this that sidesteps the problem could be:

<TabContainer defaultTab={ 1 }>
  <Tab id={ 1 }>Home</Tab>
  <Tab id={ 2 }>About</Tab>
  <Tab id={ 3 }>Contact</Tab>
  <div>
    <TabContent id={ 1 }>
      <div>Welcome to the homepage</div>
    </TabContent>
    <TabContent id={ 2 }>
      <div>Welcome to the about page</div>
    </TabContent>
    <TabContent id={ 3 }>
      <div>Welcome to the contact page</div>
    </TabContent>
  </div>
</TabContainer>

But I like how the existing API enforces content for each tab by design, so avoids any potential issues of having a Tab with no corresponding TabContent, or a defaultTab with no corresponding Tab. Perhaps me attempting to coerce the components into this API has caused this problem and I should change course, but this _was_ possible without calling setValue from the render method using the old context API, so it is slightly irksome that I can't seem to achieve it with the new context API.

In answer to your second question, I guess the question specific to this scenario is "what should happen if you specify multiple defaultTab props?":

  <Tab label="Home" defaultTab={ true }>
    <div>Welcome to the homepage</div>
  </Tab>
  <Tab label="About" defaultTab={ true }>
    <div>Welcome to the about page</div>
  </Tab>
  <Tab label="Contact">
    <div>Welcome to the contact page</div>
  </Tab>

I don't think it would be unreasonable to say that this is undefined behaviour; in the same vein, the HTML spec does not seem to define behaviour for the case that multiple radio buttons with the same name attribute have a checked attribute:

<input type="radio" id="option1" name="option" value="option1" checked>
<label for="option1">Option 1</label>

<input type="radio" id="option2" name="option" value="option2" checked>
<label for="option2">Option 2</label>

<input type="radio" id="option3" name="option" value="option3">
<label for="option3">Option 3</label>

Although most browsers seem to simply check the last one, I don't think the behaviour is guaranteed, so personally I don't see it as an issue if this implementation creates a race condition in that scenario, as it will always resolve to some sort of usable state.

Why do you need those id attributes? Couldn't you model it like this?

<TabContainer defaultTabIndex={0}>
  <Tab>Home</Tab>
  <Tab>About</Tab>
  <Tab>Contact</Tab>
  <div>
    <TabContent>
      <div>Welcome to the homepage</div>
    </TabContent>
    <TabContent>
      <div>Welcome to the about page</div>
    </TabContent>
    <TabContent>
      <div>Welcome to the contact page</div>
    </TabContent>
  </div>
</TabContainer>

A couple of issues with that:

  • How will the Tab know what index it is to know if it is active? (assuming we are looping through the children of TabContainer to determine this index?)
  • If you loop through the children of TabContainer to determine this index, what if you want to wrap Tabs in other elements?

@leonaves , you might want to check out how we've implemented the same thing in react-bootstrap using the new Context (in the v4-support branch) https://github.com/react-bootstrap/react-bootstrap/tree/v4-support/src Specifically the TabContainer, Nav, NavLink and TabPane components

Ok, now I read your original API...

<TabContainer>
  <Tab label="Home" defaultTab={ true }>
    <div>Welcome to the homepage</div>
  </Tab>
  <Tab label="About">
    <div>Welcome to the about page</div>
  </Tab>
  <Tab label="Contact">
    <div>Welcome to the contact page</div>
  </Tab>
  <div>
    <TabContent />
  </div>
</TabContainer>

...and I wonder how do you get the content (children) of the active tab at any given time to be available for the <TabContent /> to render. The only way I can think of right now is that you also store the children of the active tab in the context. Is that it?

Also, how does the Tab know with that setup if it is active at any given time?

Yep:

class TabList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      content: null,
      setContent: this.setContent,
    };
  }

  setContent = (content) => {
    this.setState({ content });
  };

  render = () => (
    <TabContext.Provider value={ this.state }>
      { this.props.children }
    </TabContext.Provider>
  );
}

const Tab = ({ label, defaultTab, children }) => (
  <TabContext.Consumer>
    { ({ content, setContent }) => {
      if (children && !content && defaultTab) {
        setContent(children);
      }

      const active = (content === children);

      return (
        <li
          onClick={ () => setContent(children }
          className={ active && 'active' }
        >
          { label }
        </li>
      );
    } }
  </TabContext.Consumer>
);

const TabContent = () => (
  <TabContext.Consumer>
    { value => value.content }
  </TabContext.Consumer>
);

It's a little unconventional, given the state is now not serialisable, but I've seen it recommended to put functions (such as setValue in my original example) in state, so it's already not serialisable. The content === children check is also a bit whiffy, but in theory it's solid, they're simply object references, so there should be no risk of a false positive.

The main part I'm not happy about is the equivalent of the setValue call in my initial example:

if (children && !content && defaultTab) {
  setContent(children);
}

It just feels wrong to be calling something that will essentially re-call the render method right in the render method, but I can't think of any other way to do it, again, unless I'm completely going about this wrong.

@jquense I have taken a brief look over your implementation, and obviously there's a lot of moving parts to it, so I may be mistaken here, but I gather it's not really possible to separate the tabs from the tab content in the rendered markup due to this section in the render method of the Tabs component:

<Nav {...props} role="tablist" as="nav">
  {ValidComponentChildren.map(children, this.renderTab)}
</Nav>

<TabContent>
  {ValidComponentChildren.map(children, child => {
    const childProps = { ...child.props };
    delete childProps.title;
    delete childProps.disabled;
    delete childProps.tabClassName;

    return <TabPane {...childProps} />;
  })}
</TabContent>

Again though, I haven't fully conceptualised all the things going on here, and I gather it's possible to substitute out certain components with custom components that may make that possible?

@leonaves the Tabs component is a shortcut the TabContainer, TabPane and Nav can be used independently to create whatever you want, like this example: https://github.com/react-bootstrap/react-bootstrap/blob/v4-support/www/src/examples/Tabs/LeftTabs.js

Ah okay, so what you've essentially got there is like my second example in this comment, but then the Tabs and Tab components wrap that to create an API similar to my original API. However in both scenarios the tabs must be assigned an explicit ID which the container must be told about to define which is the default.

I think the core of it comes down to this in my previous comment:

But I like how the existing API enforces content for each tab by design, so avoids any potential issues of having a Tab with no corresponding TabContent, or a defaultTab with no corresponding Tab. Perhaps me attempting to coerce the components into this API has caused this problem and I should change course, but this was possible without calling setValue from the render method using the old context API, so it is slightly irksome that I can't seem to achieve it with the new context API.

I'll change to assigning tabs explicit IDs if that's what needs to be done, but as I say above it is just slightly frustrating that this was previously possible and now the API has changed it is no longer possible without the above anti-pattern of updating state in the render method.

Also sorry for the very late reply, I've been on holiday.

y u do dis :(

I got lost in the discussion — is there some action item here? It seems like it was more of a question and the discussion stalled.

I think I was probably going overboard in terms of explanation of this specific issue, but it does seem like a use case that the previous context API supported that the new doesn't. That being said, the whole pattern could be bad practice and it's absolutely fine for the new API to not cover this use case, and an intentional change. Would be good to know if that's the case though.

I can provide a basic example in both the old and new API if easier? Unless you think it's not really an actionable issue?

You can probably set the value from componentDidMount in the child?

But if I tried to do the initialisation in the ValueUpdaterInput component's componentDidMount method, it would be called on every render, surely, as ValueUpdaterInput would be re-rendered each time?

I don't understand this part. componentDidMount only fires on mount (once), not on every render.

Sorry I didn't mean to say "re-rendered", I meant re... initialised? I.e:

({ value, setValue }) => (
  <ValueUpdaterInput setValue={ setValue } value={ value } />
)

Does this function not create a new instance of the ValueUpdaterInput component whenever it is called? Although writing this now, I'm thinking perhaps I'm missing something about react's internals, as by the same logic, a child inside a parent's render method would also be re-initialised each time render is called...

Yeah you've literally answered this already. Sorry for taking up your time. This makes much more sense coming back to it with the wisdom of hindsight.

Haha no problem. Yeah, React will only create one instance if the element is rendered in the same "place" with the same type and key. Otherwise every single update would lose the state in every component.

By the way, contextType in 16.6 makes this way easier, thank you!

Was this page helpful?
0 / 5 - 0 ratings