Updating react context from consumer componentDidMount causes infinite re-renders

Viewed 2178

I'm trying out some state management in React using the Context API; what I want to achieve is that when I reach a specific route I load data from the server, store it in the context, and display it in the page itself. This is causing an infinite loop where the request to the server is done over and over (and never stops).

I'm trying to use higher order components for the provider and consumer logic:

import React, { Component, createContext } from 'react';

import RequestStatus from '../RequestStatus';
import { getData } from '../Api';

const dataCtx = createContext({
  data: [],
  getData: () => {},
  requestStatus: RequestStatus.INACTIVE,
});
export default dataCtx;

export function dataContextProvider(WrappedComponent) {
  return class extends Component {
    constructor(props) {
      super(props);

      this.state = {
        data: [],
        getData: this.getData.bind(this),
        requestStatus: RequestStatus.INACTIVE,
      };
    }

    async getData() {
      this.setState({ requestStatus: RequestStatus.RUNNING });
      try {
        const data = await getData();
        this.setState({ data, requestStatus: RequestStatus.INACTIVE });
      } catch (error) {
        this.setState({ requestStatus: RequestStatus.FAILED });
      }
    }

    render() {
      return (
        <dataCtx.Provider value={this.state}>
          <WrappedComponent {...this.props} />
        </dataCtx.Provider>
      );
    }
  };
}

export function dataContextConsumer(WrappedComponent) {
  return function component(props) {
    return (
      <dataCtx.Consumer>
        {dataContext => <WrappedComponent dataCtx={dataContext} {...props} />}
      </dataCtx.Consumer>
    );
  };
}

the provider is the App component itself:

import React, { Fragment } from 'react';

import { dataContextProvider } from './contexts/DataContext';
import { userContextProvider } from './contexts/UserContext';

import AppRoutes from './AppRoutes';

function App() {
  return (
    <Fragment>
      <main>
        <AppRoutes />
      </main>
    </Fragment>
  );
}

export default userContextProvider(dataContextProvider(App));

and here's the consumer that causes the loop:

import React, { Component } from 'react';

import RequestStatus from './RequestStatus';
import { dataContextConsumer } from './contexts/DataContext';

class DataList extends Component {
  async componentDidMount() {
    const { dataCtx: { getData } } = this.props;
    await getData();
  }

  render() {
    const { dataCtx: { data, requestStatus } } = this.props;
    return (
      {/* display the data here */}
    );
  }
}

export default dataContextConsumer(DataList);

I've tried switching away from the HOC for the consumer, but it didn't help:

import React, { Component } from 'react';

import RequestStatus from './RequestStatus';
import dataCtx from './contexts/DataContext';

class DataList extends Component {
  async componentDidMount() {
    const { getData } = this.context;
    await getData();
  }

  render() {
    const { data, requestStatus } = this.context;
    return (
      {/* display the data here */}
    );
  }
}

DataList.contextType = dataCtx;

export default DataList;

The DataList is only one of the pages from where I'd like to trigger a context update.

I'm guessing that the Provider is causing a re-render of the whole App, but why? Where am I going wrong, and how can I fix this?

3 Answers

Ok, after trying to replicate the problem in a sandbox I realized what the problem was: I was wrapping a parent component in a HOC inside a render function, like so:

<Route exact path="/datapage" component={requireLoggedInUser(Page)} />

which forced the DataList component to be destroyed + recreated every time the App re-rendered.

the request loop happens because the DataList component gets re-rendered, calling ComponentDidMount, which calls getData() after each render.

A component renders if there is a change to the props or state of the component.

getData() sets the state property requestStatus (which is why your whole app gets re-rendered) which is a prop of DataList - causing a re-render of DataList.

you should not use requestStatus as a prop of DataList as you are getting that from the context anyway.

This could be because of the fact that your provider (dataContextProvider) level function getData has the same namespace as your function that you are importing from ../Api.

And then I believe that when the following line const data = await getData(); runs within the code block below, it actually calls the providers getData function, thus causing a loop.

  async getData() {
      this.setState({ requestStatus: RequestStatus.RUNNING });
      try {
        const data = await getData();
        this.setState({ data, requestStatus: RequestStatus.INACTIVE });
      } catch (error) {
        this.setState({ requestStatus: RequestStatus.FAILED });
      }
    }
Related