React not re-rendering after state update

Viewed 94

I'm currently learning React. In my homepage component, I'm using a hook to initialize and populate the state. I have 2 states, 1 contains a random pokemon and another is an array of 20 random pokemons. The first one works fine, but not the array.

Here's the homepage component:

// Hook
import { useHomeFetch } from "../hooks/useHomeFetch";
// Image
import NoImage from '../images/missingno.png';

const Home = () => {
    const { state, collection, loading, error } = useHomeFetch();
    return (
        <>
            { state.heroPokemon ?
            <HeroImage 
                image={`${API.fetchSprite(state.heroPokemon)}`}
                title={state.heroPokemon.name}
                text='Placeholder, should include abilities, etc. to show the pokemon off.'
            />
            : null}
            { collection.pokemons[0] ?
            <Grid header='Popular pokemons'>
                {collection.pokemons.map( pokemon => (
                    <h3 key={pokemon.id}>{pokemon.name}</h3>
                ) )}
            </Grid>
            : null}
        </>
    );
}

export default Home;

The heroPokemon works fine after I added the conditional rendering. My first thought was to do the same thing with the collection, as it might have rendered before all of the API call promises have resolved.

If I look at the react dev tools, this is what I see for the state:

hooks
HomeFetch

1 State : {heroPokemon: {…}}
2 State : {pokemons: Array(20)}
           pokemons :
               [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, …]

So it looks to me that the call indeed completed fine with 20 random pokemon objects in there.

Here's is the hook:

const initialState = {
    heroPokemon: null
}

const fetchRandomPokemons = async (ids) => {
    let pokemons = [];
    ids.forEach(async num => {
        pokemons.push(await API.fetchPokemon(num));
    });
    return pokemons;
}

export const useHomeFetch = () => {
    const [state, setState] = useState(initialState);
    const [collection, setCollection] = useState({pokemons: []});
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(false);
    
    const fetchPokemons = async (limit = 20) => {
        try {
            setError(false);
            setLoading(true);
            
            const heroPokemon = await API.fetchPokemon(randomInt(1, 898));
            const randomInts = randomUniqueInt(1, 898, limit);
            const pokemons = await fetchRandomPokemons(randomInts);

            setState(prev => ({heroPokemon}));
            setCollection(prev => ({pokemons}));
        } catch (error) {
            setError(true);
        }
        setLoading(false);
        
    };
    // Initial render
    useEffect(() => {
        fetchPokemons(20);
    }, [])

    return { state, collection, loading, error };
};

When I take out the conditional, Grid shows up, but it doesn't have the h3s with the pokemon names in them. If I console.log out state and collection right before the return in home. The last few ones show the correct state.heroPokemon and collection.pokemons populated correctly. This leads me to believe that the states are updated correctly, but why is React not re-rendering the grid component?

To add, I also tried adding this:

<p>{collection.pokemons[0]}</p>

To the home component, and nothing shows up. I feel like I may be misunderstanding or missing a crucial part of how states and re-renders work, which is leading me to missing what I could be doing wrong.

EDIT: Here's what the API call returns: pokeapi.co/api/v2/pokemon/ditto I also debugged Home and this is what it looks like: imgur.com/a/jVhaSsk

I'm confused now because pokemon.name clearly exists and is not undefined. Why are they not populating in the h3s?

EDIT 2: I changed the home component to this:

<Grid header='Popular pokemons'>
{
    state.results.map(pokemon => {
        console.log(pokemon.name)
        return <h3>pokemon name</h3>
     })
}
</Grid>

And it looks like the h3s are still empty. Is it still waiting for the promises in the array? But the console.logs are printing out the pokemon names fine.

EDIT 3: Thanks to Drew, I found where the problem lies. This is what my Grid component looks like:

import React from "react";

import { Wrapper, Content } from './Grid.styles';

const Grid = ({ header }) => (
    <Wrapper>
        <h1>{header}</h1>
    </Wrapper>
);

export default Grid;

I'm using styled-components as well, and here's the file:

import styled from 'styled-components';

export const Wrapper = styled.div`
    max-width: var(--maxWidth);
    margin: 0 auto;
    padding: 0 20px;

    h1 {
        color: var(--medGrey);

        @media screen and (max-width: 768px) {
            font-size: var(--fontBig);
        }
    }
`;

export const Content = styled.div`
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    grid-gap: 2rem;
`;

As you can see, Content isn't in Wrapper. After I changed it to:

const Grid = ({ header, children }) => (
    <Wrapper>
        <h1>{header}</h1>
        <Content>{children}</Content>
    </Wrapper>
);

It worked perfectly. It looks like the array of Pokemons are being passed to Grid as {children}, so when it doesn't exist well it doesn't render it.

2 Answers

Issue

It seems the issue is in your fetchRandomPokemons utility function. Even though you've declared fetchRandomPokemons async you are not waiting for anything. Array.prototype.forEach is also completely synchronous as well. You did however declare the .forEach callback to async and await the API response, but because .forEach is synchronous the fetchRandomPokemons function has already returned the empty pokemons array and resolved. The async keyword only allows a function scope to use the await keyword.

const fetchRandomPokemons = async (ids) => {
  let pokemons = [];
  ids.forEach(async num => { // <-- synchronous
    pokemons.push(await API.fetchPokemon(num));
  });
  return pokemons; // <-- returns []
}

...

const pokemons = await fetchRandomPokemons(randomInts); // <-- []

setCollection({ pokemons }); // <-- { pokemons: [] }

What you are logging is the pokemons array that the .forEach array mutated when each API.fetchPokemon Promise resolved.

Solution

Map the ids to an array of Promises and wait them all to resolve and return the resolved array of pokemons.

const fetchRandomPokemons = (ids) => {
  return Promise.all(ids.map(API.fetchPokemon));
}

...

const fetchPokemons = async (limit = 20) => {
  setError(false);
  setLoading(true);

  try {
    const heroPokemon = await API.fetchPokemon(randomInt(1, 898));
    const randomInts = randomUniqueInt(1, 898, limit);
    const pokemons = await fetchRandomPokemons(randomInts);

    setState({ heroPokemon });
    setCollection({ pokemons });
  } catch (error) {
    setError(true);
  }
  setLoading(false);
};

Update

Adding running codesandbox and screencap.

Edit react-not-re-rendering-after-state-update

enter image description here

Try this:

 // Hook
import { useHomeFetch } from "../hooks/useHomeFetch";
// Image
import NoImage from '../images/missingno.png';

const Home = () => {
    const [newCollection, setNewCollection] =useState([])
    const { state, collection, loading, error } = useHomeFetch();
    
    useEffect(()=>{
       setNewCollection(collection || []);
     },[collection])

 
    return (
        <>
            { state.heroPokemon ?
            <HeroImage 
                image={`${API.fetchSprite(state.heroPokemon)}`}
                title={state.heroPokemon.name}
                text='Placeholder, should include abilities, etc. to show the pokemon off.'
            />
            : null}
            { newCollection.pokemons[0] ?
            <Grid header='Popular pokemons'>
                {newCollection.pokemons.map( pokemon => (
                    <h3 key={pokemon.id}>{pokemon.name}</h3>
                ) )}
            </Grid>
            : null}
        </>
    );
}

export default Home;

if still not work than share your repo. so I can help you more. Thanks

Related