Adrian Pascu
Adrian Pascu

Reputation: 1039

React won't rerender when Redux state changes

Within a react app I have a mapping of some data fetched from a Redux store to some component

{this.props.team && this.props.team.map((value: User, index: number) =>
                                    (<Card key={index} className="team-card">
                                        <CardMedia style={{
                                            backgroundImage: `url(${value.photoURL})`
                                        }} />
                                        <Typography use={"headline4"}>{value.displayName}</Typography>
                                        <Typography use={"body1"}>{value.description}</Typography>
                                        <CardActions>
                                            <CardActionButtons>
                                                {/* TODO: Add the ability to go to About to a specific team member card */}
                                                <CardActionButton>Vezi profilul</CardActionButton>
                                            </CardActionButtons>
                                        </CardActions>
                                    </Card>)
                                )}

Here team is a prop mapped from a redux Store. The data in the Redux store gets fetched from the database when the user opens the App. This works, since I've logged the changes of the team prop and it actually updates as expected.

The problem is that even after the prop gets updated,which happens maybe a second after the initial render, the app won't re-render to reflect this prop change. But if after that this component get's unmounted and remounted it gets rendered correctly. Also between the unmounting and remounting the redux store doesn't get updated and nothing happens in the mounting lifecycle.

Does anyone have any idea what may cause this behavior? Thanks in advance!

Update:

Here is the full component (it uses Typescript)

import React from "react"
import { Article } from "../../models/Article";
import Carousel, { CarouselItem } from "../Carousel/Carousel";


import "./Homescreen.scss";
import { connect } from "react-redux";
import AppState from "../../store/AppState";
import { Typography, Card, CardMedia, CardActionButton, CardActions, CardActionButtons } from "rmwc"
import User from "../../models/User";

import ArticleCompact from "../Article/ArticleCompact/ArticleCompact";
import Navbar from "../Navbar/Navbar";

class Homescreen extends React.Component<HomescreenProps, {}>{

    constructor(props: Readonly<HomescreenProps>) {
        super(props);

    }

    render() {
        return (
            <main>
                <Navbar></Navbar>
                <div id="container">
                    <div id="content">
                        <Carousel className="homescreen-carousel" items={this.props.carouselItems} speed={5}></Carousel>
                        {this.props.recentArticles.length !== 0 && (<section id="homescreen-recent-articles">
                            <Typography use={"headline2"} className="homescreen-head">Recente</Typography>
                            <hr className="homescreen-hr" />
                            {this.props.recentArticles[0] && (
                                <ArticleCompact URL={"/article/" + this.props.recentArticles[0].url} image={this.props.recentArticles[0].coverURL}
                                    text={this.props.recentArticles[0].shortVersion} title={this.props.recentArticles[0].title}
                                    type={"left-to-right"}
                                />)}
                            {this.props.recentArticles[1] && (<ArticleCompact URL={"/article/" + this.props.recentArticles[1].url} image={this.props.recentArticles[1].coverURL}
                                text={this.props.recentArticles[1].shortVersion} title={this.props.recentArticles[1].title}
                                type={"right-to-left"}
                            />)}
                        </section>)}
                        <section id="homescreen-team">
                            <Typography use={"headline2"} className="homescreen-head">Echipa</Typography>
                            <hr className="homescreen-hr" />
                            <div id="team-cards">
                                {this.props.team && this.props.team.map((value: User, index: number) =>
                                    (<Card key={index} className="team-card">
                                        <CardMedia style={{
                                            backgroundImage: `url(${value.photoURL})`
                                        }} />
                                        <Typography use={"headline4"}>{value.displayName}</Typography>
                                        <Typography use={"body1"}>{value.description}</Typography>
                                        <CardActions>
                                            <CardActionButtons>
                                                {/* TODO: Add the ability to go to About to a specific team member card */}
                                                <CardActionButton>Vezi profilul</CardActionButton>
                                            </CardActionButtons>
                                        </CardActions>
                                    </Card>)
                                )}
                            </div>
                        </section>
                    </div>
                </div>
            </main>
        )
    }
}



function mapStateToProps(state: Readonly<AppState>) {
    const items: CarouselItem[] = [] as CarouselItem[];
    const articles: Article[] = [];
    if (state.articles.featured.length !== 0)
        state.articles.featured.map((item: Article) => {
            return {
                image: item.coverURL,
                title: item.title,
                path: "/article/"+item.url
            }
        }
        ).forEach((value: CarouselItem) => {
            items.push(value);
        })

    //Map the first 4 recent articles to CarouselItems and push them to an array
    state.articles.recent.map(async (item: Article) => (
        {
            image: URL.createObjectURL(await fetch(item.coverURL).then(res => res.blob())),
            title: item.title,
            path: "/article/"+item.url
        })
    ).forEach(async (value, index) => {
        if (index < 4)
            items.push(await value);
    });

    //Map the last 2 recent articles to props
    for (let [index, article] of state.articles.recent.entries()) {
        if (index >= 4)
            articles.push(article)
    }

    return {
        carouselItems: items,
        recentArticles: articles,
        team: state.metadata.team
    }
}

export default connect(mapStateToProps)(Homescreen);

Also here is the reducer responsible for the updates of that store property

export default function userReducer(state: Readonly<MetadataState> | undefined = initialAppState.metadata, action: MetadataActions): MetadataState {
    switch (action.type) {
        case 'TEAM_RECIEVED': return { ...state, team: action.payload };
        default: return state;
    }
}

Update #2 :

Here is the action that dispathces TEAM_RECIEVED

export function retrieveTeam() {
    return async (dispatch: Dispatch) => {

        const team = await getTeam_firestore();
        const mappedTeam: User[] = [];
        team.forEach(async (val: User, index: number) => mappedTeam.push({
            ...val,
            photoURL: val.photoURL !== null ? URL.createObjectURL(await fetch(val.photoURL!!).then(res => res.blob())) : null
        }));
        console.log('Got team')

        return dispatch({
            type: 'TEAM_RECIEVED',
            payload: mappedTeam
        })
    }
}

Upvotes: 1

Views: 373

Answers (1)

Brandon
Brandon

Reputation: 39222

Your async action is buggy. In particular this code:

team.forEach(async (val: User, index: number) => mappedTeam.push({
        ...val,
        photoURL: val.photoURL !== null ? URL.createObjectURL(await 
           fetch(val.photoURL!!).then(res => res.blob())) : null
    }));

is going to asynchronously mutate the store state outside of any actions sometime in the future. This is not allowed. Try this version instead.

export function retrieveTeam() {
    return async (dispatch: Dispatch) => {

        const team = await getTeam_firestore();
        const mappedTeam: User[] = await Promise.all(team.map(
            async (val: User, index: number) => {
              const result = {...val};
              if (result.photoURL !== null) {
                const response = await fetch(result.photoURL);
                const blob = await response.blob();
                result.photoURL = URL.createObjectURL(blob);
              }
              return result;
        }));

        console.log('Got team')

        return dispatch({
            type: 'TEAM_RECIEVED',
            payload: mappedTeam
        })
    }
}

This version awaits the async fetching before dispatching the TEAM_RECIEVED action.

A bit more explanation:

array.foreach(async function) will just queue a bunch of async work but foreach will return immediately. You need to await all of the async work. So you cant use array.foreach(). The solution is one of these 2 patterns:

Assume you have this method:

async function getValWithPhoto(val) {
  const result = {...val};
  if (result.photoURL !== null) {
     const response = await fetch(result.photoURL);
     const blob = await response.blob();
     result.photoURL = URL.createObjectURL(blob);
  }
  return result;
}

Pattern 1 - run each async fetch in serial order (one at a time):

const mappedTeam = [];
for (const val of team) {
  const mappedVal = await getValWithPhoto(val);
  mappedTeam.push(mappedVal);
}

return dispatch(...);

Pattern 2 - run all the fetch jobs in parallel (at the same time) (what I did in my answer above):

const arrayOfPromises = team.map(val => getValWithPhoto(val));
// Use Promise.all() to turn the array of promises into a single
// promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
const promise = Promise.all(arrayOfPromises);
// now await that promise, which will return array of results
const mappedTeam = await promise;
return dispatch(...);

Upvotes: 2

Related Questions