【问题标题】:React won't rerender when Redux state changes当 Redux 状态改变时 React 不会重新渲染
【发布时间】:2020-01-20 18:40:10
【问题描述】:

在一个 react 应用程序中,我有一个从 Redux 商店获取的一些数据到某个组件的映射

{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>)
                                )}

这里的 team 是一个从 redux Store 映射的道具。当用户打开应用程序时,从数据库中获取 Redux 存储中的数据。这行得通,因为我已经记录了 team 道具的更改,并且它实际上按预期更新。

问题在于,即使在 prop 更新后(可能在初始渲染后一秒钟发生),应用程序也不会重新渲染以反映此 prop 更改。但是,如果在那之后这个组件被卸载并重新安装,它就会被正确渲染。同样在卸载和重新挂载之间,redux 存储不会更新,并且在挂载生命周期中没有任何反应。

有谁知道可能导致这种行为的原因?提前致谢!

更新:

这是完整的组件(它使用 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);

这里还有负责更新 store 属性的 reducer

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;
    }
}

更新#2:

这是让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
        })
    }
}

【问题讨论】:

  • 显示您的mapStateToProps() 以及相应的reducer。
  • 你的组件应该在你的道具改变时重新渲染。如果由于某种原因没有发生,则将道具置于状态并在道具更改时更新状态以手动处理渲染。
  • @AlexanderStaroselsky 我已经添加了它们
  • 如果您在mapStateToProps() 中记录state.metadata.team,您是否看到数据发生了变化?
  • 我注意到您的 mapStatetoProps 正在对某些字段使用 async/await。你不能做这个。 mapStateToProps 必须完全同步。如果您有异步工作,请在异步操作中执行它们并使用结果更新 redux 状态...

标签: javascript reactjs redux


【解决方案1】:

您的异步操作有问题。特别是这段代码:

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
    }));

将在未来某个时间在任何操作之外异步改变存储状态。这是不允许的。试试这个版本吧。

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
        })
    }
}

此版本在调度 TEAM_RECIEVED 操作之前等待异步获取。

再解释一下:

array.foreach(async function) 只会将一堆异步工作排队,但 foreach 会立即返回。您需要等待所有异步工作。所以你不能使用array.foreach()。解决方案是以下两种模式之一:

假设你有这个方法:

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;
}

模式 1 - 以串行顺序运行每个异步获取(一次一个):

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

return dispatch(...);

模式 2 - 并行(同时)运行所有 fetch 作业(我在上面的回答中所做的):

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(...);

【讨论】:

  • 谢谢!这行得通。但是,您能否进一步解释一下为什么会这样?我看到主要区别在于Promise.all 的使用(从未使用过它,所以我不太了解它是如何工作的)。我理解您的解决方案的问题来自于我也在等待获取,而没有使用Promise.all
猜你喜欢
  • 2020-04-22
  • 2018-12-31
  • 1970-01-01
  • 1970-01-01
  • 2019-05-22
  • 2020-03-03
  • 1970-01-01
  • 1970-01-01
  • 2018-06-04
相关资源
最近更新 更多