Lifecycle hooks - Where to set state?

I am trying to add sorting to my movie app, I had a code that was working fine but there was too much code repetition, I would like to take a different approach and keep my code DRY. Anyways, I am confused as on which method should I set the state when I make my AJAX call and update it with a click event.

This is a module to get the data that I need for my app.

export const moviesData = {
  popular_movies: [],
  top_movies: [],
  theaters_movies: []
};

export const queries = {
  popular:
    "https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=###&page=",
  top_rated:
    "https://api.themoviedb.org/3/movie/top_rated?api_key=###&page=",
  theaters:
    "https://api.themoviedb.org/3/movie/now_playing?api_key=###&page="
};

export const key = "68f7e49d39fd0c0a1dd9bd094d9a8c75";

export function getData(arr, str) {
  for (let i = 1; i < 11; i++) {
    moviesData[arr].push(str + i);
  }
}

The stateful component:

class App extends Component {
  state = { 
   movies = [],
   sortMovies: "popular_movies",
   query: queries.popular,
   sortValue: "Popularity"
  }
}

// Here I am making the http request, documentation says
// this is a good place to load data from an end point
async componentDidMount() {
    const { sortMovies, query } = this.state;
    getData(sortMovies, query);

    const data = await Promise.all(
      moviesData[sortMovies].map(async movie => await axios.get(movie))
    );
    const movies = [].concat.apply([], data.map(movie => movie.data.results));
    
    this.setState({ movies });
  }

In my app I have a dropdown menu where you can sort movies by popularity, rating, etc. I have a method that when I select one of the options from the dropwdown, I update some of the states properties:

handleSortValue = value => {
    let { sortMovies, query } = this.state;

    if (value === "Top Rated") {
      sortMovies = "top_movies";
      query = queries.top_rated;
    } else if (value === "Now Playing") {
      sortMovies = "theaters_movies";
      query = queries.theaters;
    } else {
      sortMovies = "popular_movies";
      query = queries.popular;
    }

    this.setState({ sortMovies, query, sortValue: value });
  };

Now, this method works and it is changing the properties in the state, but my components are not re-rendering. I still see the movies sorted by popularity since that is the original setup in the state (sortMovies), nothing is updating.

I know this is happening because I set the state of movies in the componentDidMount method, but I need data to be Initialized by default, so I don’t know where else I should do this if not in this method.

I hope that I made myself clear of what I am trying to do here, if not please ask, I’m stuck here and any help is greatly appreciated. Thanks in advance.

I think you should create a separate method that does everything componentDidMount is doing. Then you call it from componentDidMount itself and from handleSortValue.

You may try to use componentWillUpdate, but I don’t think it makes too much sense because you shouldn’t be loading data on every update, just when the component is mounted the first time and when the user select a different option from the dropdown menu.

But where should I set the movies state? I think I still need to set it in componentDidMount because I need to have some data by default. The issue with setting the movies state in this method is that I can’t update it :confused:

You’ll still use componentDidMount, you just call the method you created.

async componentDidMount() {
  fetchData();
}

handleSortValue = value => {
    let { sortMovies, query } = this.state;

    if (value === "Top Rated") {
      sortMovies = "top_movies";
      query = queries.top_rated;
    } else if (value === "Now Playing") {
      sortMovies = "theaters_movies";
      query = queries.theaters;
    } else {
      sortMovies = "popular_movies";
      query = queries.popular;
    }

    this.setState({ sortMovies, query, sortValue: value });
    fetchData();
  };

async fetchData() {
    const { sortMovies, query } = this.state;
    getData(sortMovies, query);

    const data = await Promise.all(
      moviesData[sortMovies].map(async movie => await axios.get(movie))
    );
    const movies = [].concat.apply([], data.map(movie => movie.data.results));
    
    this.setState({ movies });
}

I just copy/paste stuff here to show kind of how it would look like, obvious you may have to adjust some stuff. Also, componentDidMount is async, this means you won’t have data by default, react is rendering your component before any data is available and then updating it when the data arrives and you call this.setState.

Maybe I misunderstand your question? Let me know.

Oh yeah I see what you did there, this kinda works. A few things though:

1- Do I really need to async the componentDidMount? I am already fetching the data in another method.

2 - Is this a fine solution? After a quick testing I have found 2 bugs at least, I am not sure if this has anything to do with the way we are doing this or if there’s something wrong with my handleSortValue method.

I need to click twice one of the items in the dropdown menu to update the UI with the sorted data, plus if I click the same item consecutively it crashes:

Maybe I can end the function execution if the same item is selected to avoid this bug. And yes, you have the idea right. I had a working code but I was repeating way too much I think:

Original Code
async componentDidMount() {
    const data = await Promise.all(
      popularMovies.map(async movie => await http.get(movie))
    );

    const popular = [].concat.apply([], data.map(movie => movie.data.results));

    const data2 = await Promise.all(
      topRatedMovies.map(async movie => await http.get(movie))
    );

    const topRated = [].concat.apply(
      [],
      data2.map(movie => movie.data.results)
    );

    const data3 = await Promise.all(
      theaterMovies.map(async movie => await http.get(movie))
    );

    const theater = [].concat.apply([], data3.map(movie => movie.data.results));

    const moviesData = { ...this.state.moviesData };
    moviesData.popularMovies = popular;
    moviesData.topRatedMovies = topRated;
    moviesData.theaterMovies = theater;

    const genres = [{ id: "", name: "All genres" }, ...getGenres()];

    this.setState({ moviesData, genres });

It was working smoothly but you can see all the repetition.

umm I think I know what’s going on with this implementation, I’m not resetting the movies array to 0 or changing its value when I click one of the dropdown items, in fact I am adding more data to it, that’s why it crashes.

It should always be 200 and pagination should show 10 pages only, so I need to find a way to fix that.

  1. You don’t need async in componentDidMount, you use async only when you need to use the await keyword. Or you could await the fetchData call, it won’t make any difference though.

  2. It is a fine solution. When we want to share functionality, the first option that comes to mind is to create a function or a class.

About the bugs, it was expected since I just copied and paste the code just to show you what I was thinking. If you can’t fix something post here and I’ll try to help you out.

I’ve been trying to figure out a way to fix this, I thought that by adding this piece of code here

if (movies.length > 200) {
      movies = movies.splice(0, 200);
    }

to the fetchData method would solve the issue, so:

async fetchData() {
    const { sortMovies, query } = this.state;
    getData(sortMovies, query);

    const data = await Promise.all(
      moviesData[sortMovies].map(async movie => await http.get(movie))
    );

    let movies = [].concat.apply([], data.map(movie => movie.data.results));
    if (movies.length > 200) {
      movies = movies.splice(0, 200);
    }

    this.setState({ movies });
  }

And even though now the array length will no longer be > 200, I still need to click twice to update the UI when I select an item from the dropdown menu. If I remove the first 200 items, is it not supposed to show the other 200 items?

Ok I added a console.log() to see what’s going on with the sortMovies property

 async fetchData() {
    const { sortMovies, query } = this.state;
    getData(sortMovies, query);
    console.log(sortMovies);

    const data = await Promise.all(
      moviesData[sortMovies].map(async movie => await http.get(movie))
    );

    let movies = [].concat.apply([], data.map(movie => movie.data.results));
    if (movies.length > 200) {
      movies = movies.splice(0, 200);
    }

    this.setState({ movies });
  }

To my surprise, when I click any item other than popularity, it still returns popularity, so state it’s not updating right away.

handleSortValue = value => {
    let { sortMovies, query } = this.state;

    if (value === "Top Rated") {
      sortMovies = "top_movies";
      query = queries.top_rated;
    } else if (value === "Now Playing") {
      sortMovies = "theaters_movies";
      query = queries.theaters;
    } else {
      sortMovies = "popular_movies";
      query = queries.popular;
    }
    this.setState({ sortMovies, query, sortValue: value, currentPage: 1 });
    this.fetchData();
  };

What’s wrong with my method, why is it that sortMovies is not updating right after I click an item from the dropdown menu?, instead there’s a delay.

Can not see where it should be updating the dropdown menu, but it is highly recommended to use setState with a function instead of the way you are doing it. It is the only way to guarantee state gets updated the way you expect.

See my example of why you sometime have to use a function.

The setState() method does not immediately update the state of the component, it just puts the update in a queue to be processed later. React may batch multiple update requests together to make rendering more efficient.
https://stackoverflow.com/questions/33613728/what-happens-when-using-this-setstate-multiple-times-in-react-component

You’re calling setState two times before react has a change to update the state when calling handleSortValue.

handleSortValue = value => {
    let { sortMovies, query } = this.state;

    if (value === "Top Rated") {
      sortMovies = "top_movies";
      query = queries.top_rated;
    } else if (value === "Now Playing") {
      sortMovies = "theaters_movies";
      query = queries.theaters;
    } else {
      sortMovies = "popular_movies";
      query = queries.popular;
    }
    this.setState({ sortValue: value, currentPage: 1 }); // update only what is not used in fetchData
    this.fetchData(sortMovies, query);  // pass the parameters fetchData needs
  };

componentDidMount() {
  fetchData(this.state.sortMovies, this.state.query);
}

async fetchData(sortMovies, query) {
    getData(sortMovies, query); // use parameters instead of retrieving from state
    console.log(sortMovies);

    const data = await Promise.all(
      moviesData[sortMovies].map(async movie => await http.get(movie))
    );

    let movies = [].concat.apply([], data.map(movie => movie.data.results));
    if (movies.length > 200) {
      movies = movies.splice(0, 200);
    }

    this.setState({ movies });
  }

Again, remember that I can’t run your code to test and make sure everything is working, I’m just working with this markdown editor so you’ll have to fix things in your end. I’m just trying to give you the general idea.

So THAT was the problem which was driving me crazy. I’ve always thought React updates the state of a component as soon as one of the state props changes (let’s say, with a click event), that’s what I’ve been seen until now.

But I think what made think this way was because I’ve always used console.log() in the render() method where I can see the changes of the state immediately. Well you saved my day man, this thing was kicking my butt. Thanks a lot.

And this lead me to another question, why can I see the changes of the state immediately in the render method, but not in one of the functions/methods of my components?

Here’s an example.

class Person extends Component {
 state = {
  itemValue: "Peter"
 }

logger() {
 console.log(this.state.itemValue);
}

handleValue = value => {
 this.setState({ itemValue: value })
 this.logger();
}

render() {
 return( 
   <React.Fragment>
    <input type="submit" value={this.state.itemValue} />
    <ul>
      <li onClick={e => this.handleValue(e.currentTarget.textContent)}>John</li>
      <li onClick={e => this.handleValue(e.currentTarget.textContent)}>David</li>
    </ul>
   </React.Fragment>
  );
 }
}

When I click the first item, logger will print in the console “Peter” instead of David, then when I click again any other item, logger will print “David”, there’s the delay I was talking about. I am not even setting the state in this method, why does it behave like this?

If i use console.log(this.state.itemValue) I would see the changes of the state right after the click, without any sort of delay.

React is complex behind the scenes, so don’t make this too hard on yourself.

What is important to know is that react will guaranteed give you the updated state in your render method. So, it doesn’t update the state object after a call for setState, it only updates the state just before calling your render method.

Well that makes sense to me now. So what I take from all this is that my methods should not depend on the value of the state, because whenever I setState in a method, the state will be put into a queue and it’ll only be updated right before calling the render method, reason why in my previous example the logger method doesn’t work as expected and has a “delay”, because the state has not been updated yet in the state object.

By the way, is componentDidMount method called only once or every time the component re-renders?

Another thing, if I can change the data from my http request with a simple method like we just did here, what’s componentDidUpdate method good for? I thought here is where we should do such things.

In general I think it is good practice to call this.setState as the last thing in a method. If you want to do something that depends on state after a call to this.setState you can actually use the second argument as a callback. It should be pretty rare to use this though.

Example:

this.setState({ something: 'something' }, () => console.log(this.state.someting)) // prints 'something'

componentDidMount is called when then component is mounted, if you want to call something on every re-render you can use componentWillUpdate. componentDidMount will be called the first time the component is mounted and only if the component is unmounted and mounted again, for example if you remove it from the screen and then shows it again.

I don’t think you understood what we’re doing in that method, if you didn’t used componentDidMount to call fetchData how would you fetch your data when the app started? componentDidMount is most commonly used for fetching data, it doesn’t matter if we do it directly in componentDidMount or if we call another method from it. Its the same thing, we just use the function because we needed to share code. We could repeat all of our code in both methods instead of creating one method to share that functionality. But then our code wouldn’t be DRY.

Oh I know why componentDidMount method is the best place to fetch data, my doubt was if this method is called every time the component re-renders and you answered that question for me.

My other doubt was that I thought I had to use one of React lifecycle methods to fetch data and update it, like initialized default data in componentDidMount for when the app starts and then update the data with componentDidMount or so, that’s why it was a bit of surprised to me when you just created a regular method to make the http request and then just call it whenever it needed to be called passing the parameters. This was indeed my biggest confusion, hence why I asked what’s componentDidUpdate good for?

Like if I can just create a regular method to make http request and update it when needed, why or when should I use componentDidUpdate? I’m not sure if I explained myself good enough but I understood very well what you did with fetchData method, I just had no idea I could or should do it with a regular method.

Well, you still need to call fetchData from somewhere right? That’s why componentDidMount is useful, to fetch your data right after your component is mounted :+1:

But I am not talking about componentDidMount, I am talking about componentDidUpdate. I don’t see a real life scenario where I should use this method. Because originally I thought that’s the place (componentDidUpdate) where I should update the data of my http request after the component is mounted.

In fact that was my first approach, I fetch default data in componentDidMount and then try to update it in componentDidUpdate and the app crashed, there was some kind of infinite loop.

Right! Well, you shouldn’t worry about it, when the time come you’ll know it’s time to use it. From the docs, you’ll see that it is mostly used if you want to manipulate the dom after react has done its thing, and for saving data to the server on every change, instead of waiting the user to press submit or something like that.

In any case, here is the suggest use case from the docs:

componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.
Use this as an opportunity to operate on the DOM when the component has been updated. This is also a good place to do network requests as long as you compare the current props to previous props (e.g. a network request may not be necessary if the props have not changed).

https://reactjs.org/docs/react-component.html#componentdidupdate

Sorry I miss what you were trying to say :sweat_smile: