【问题标题】:React: Custom component not representing the right data反应:自定义组件不代表正确的数据
【发布时间】:2018-10-15 19:11:50
【问题描述】:

我现在面临一个奇怪的问题。 我有一个通过 rest api 来的任务列表。 我创建了一个自定义卡片组件来显示它。

    /* eslint-disable import/first */
import React, { Component } from 'react';
import PropTypes from 'prop-types';

import Grid from 'material-ui/Grid';
import Paper from 'material-ui/Paper';
import List, { ListItem, ListItemIcon, ListItemText } from 'material-ui/List';
import Button from 'material-ui/Button';
import Card, { CardActions, CardContent } from 'material-ui/Card';
import Typography from 'material-ui/Typography';
import TextField from 'material-ui/TextField';
import { LinearProgress } from 'material-ui/Progress';

import Modal from 'react-responsive-modal';

var Moment = require('moment');

import TaskActions from '../redux/TaskRedux'
import ProgressColumn from './Progress'
import TaskCard from './Card'
import AddTaskCard from './AddTaskCard'

import { connect } from 'react-redux'

import '../styles/main.css';


export class Container extends Component{

    constructor(props){
        super(props);

        this.state = {
            fetching: this.props.fetching,
            taskName: null,
            showError: false,
            openEditModal: false,
            editTaskName: null,
            showUpdateError: false,
            toBeUpdatedTask: null,
            progressTasks:[],
            tasks: []
        }

    }
    componentDidMount(){
        this.props.getTasks()
    }

    componentDidUpdate(prevProps, prevState) {
        if(prevProps !== this.props){
            this.setState({fetching: this.props.fetching, 
            tasks: this.props.tasks})
        }
    }

    render(){
        let {fetching, progressTasks} = this.state
        let {tasks} = this.props
        if(tasks)
            tasks.map(i => console.log(i.title))
        return (
            <div className="grid-root">

                <Grid container spacing={24}>
                    <Grid item xs={3} sm={3}>
                        <Paper className="task-list-view">
                            <List className="task-list">
                                {tasks && tasks.map((item,i) => (
                                    <ListItem key={`item-${i}`}>
                                        <TaskCard task={item} key={`item-${i}`}/>
                                    </ListItem>
                                ))}
                            </List>
                            <AddTaskCard/>
                        </Paper>
                        {fetching ? (<LinearProgress color="secondary" />): ('')}
                    </Grid>
                    <Grid item xs={3} sm={3}>
                        <Paper className="task-list-view">
                            <ProgressColumn progressTasks={progressTasks}/>
                        </Paper>
                    </Grid>
                </Grid>
            </div>
        )
    }
}


const mapStateToProps = (state) => {
    return {
      fetching: state.task.fetching,
      tasks: state.task.tasks,
    }
  }

  const mapDispatchToProps = (dispatch) => {
    return {
        getTasks: () => dispatch(TaskActions.fetchTasks()),
        deleteTask: (taskId) => dispatch(TaskActions.deleteTask(taskId)),
        updateTask: (taskId,title) => dispatch(TaskActions.updateTask(taskId,title)),
    }
  }

  export default connect(mapStateToProps, mapDispatchToProps)(Container)

这是卡片组件:

/* eslint-disable import/first */

import React, { Component } from 'react';

import { connect } from 'react-redux'

import Card, { CardActions, CardContent } from 'material-ui/Card';
import Button from 'material-ui/Button';
import Typography from 'material-ui/Typography';

import TaskActions from '../redux/TaskRedux'
import EditModal from './EditModal'

var Moment = require('moment');


export class TaskCard extends Component{

    constructor(props){
        super(props)

        this.state = {
            openEditModal: false,
            editTaskName: null,
            task: this.props.task
        }

    }

    // handle edit of task name
    handleTaskEdit(task){
        console.log("edit task", task)
        // this.setState({openEditModal: true, editTaskName: task.title, toBeUpdatedTask: task})
        this.props.editTask(task)
    }

    // handle start task button click
    // Will push the tasks for progressTasks state
    handleStartTask(task){
        // this.setState({
        //     progressTasks: [...this.state.progressTasks, task]
        // })
    }

    // handle deletion of the task
    handleTaskDelete(taskId){
        console.log(taskId)
        this.props.deleteTask(taskId)
        // this.props.getTasks()
    }


    render(){
        let {task} = this.state
        let created = Moment(task.created).format("Do MMM YYYY")
        console.log("task", task)
        return (
            <Card className="task-card" key={task.id}>
                <EditModal/>
                <CardContent>
                    <Typography variant="headline" component="h2">
                        {task.title}
                    </Typography>
                    <Typography color="textSecondary">
                        {created}
                    </Typography>
                </CardContent>
                <CardActions>
                    <Button size="small" color="primary" onClick={this.handleTaskEdit.bind(this, task)}>Edit</Button>
                    <Button size="small" color="primary" onClick={this.handleStartTask.bind(this, task)}>Start Task</Button>
                    <Button size="small" color="secondary" onClick={this.handleTaskDelete.bind(this, task.id)}>Delete</Button>
                </CardActions>
            </Card>
        )
    }
}

const mapStateToProps = (state) => {
    return {

    }
  }

  const mapDispatchToProps = (dispatch) => {
    return {
        getTasks: () => dispatch(TaskActions.fetchTasks()),
        deleteTask: (taskId) => dispatch(TaskActions.deleteTask(taskId)),
        editTask: (currentTask) => dispatch(TaskActions.editTask(currentTask))
    }
  }

  export default connect(mapStateToProps, mapDispatchToProps)(TaskCard)

当我从顶部或底部删除一项任务时,它会正常更新,但是当我从中间删除时,底部的一项会消失,而要删除的一项仍会保留/显示。当您刷新它时,当然一切都很好,但问题是,为什么会发生这种情况?我有一个 redux saga,它在删除操作后再次获取任务,我可以确认 props 也确实获得了正确的数据。

更新 1

所以,我试着从头开始调试它。看起来 TaskCard 有点缓存道具。

在添加了{item.title} 的图像中,正下方是渲染TaskCard 的位置,两者的标题不同,但{item.title} 是正确的。

更新 2

基于 jmathew, answer,我将 ListItem 和 TaskCard 的键更新为 item.id,因此删除有效,但更新仍然无效。同样,根据更新 1,它仍然显示错误的标题,但 {item.title} 是正确的。 所以,这部分代码现在看起来像:

                <List className="task-list">
                    {tasks && tasks.map((item,i) => (
                        <ListItem key={item.id}>
                            {item.title}
                            <TaskCard item={item} key= 
              {`item-${item.id}`}/>
                        </ListItem>
                    ))}
                </List>

更新 3

新的任务卡组件:

/* eslint-disable import/first */

import React, { Component } from 'react';

import { connect } from 'react-redux'

import Card, { CardActions, CardContent } from 'material-ui/Card';
import Button from 'material-ui/Button';
import Typography from 'material-ui/Typography';

import TaskActions from '../redux/TaskRedux'
import EditModal from './EditModal'

var Moment = require('moment');


export class TaskCard extends Component{

    constructor(props){
        super(props)

        this.state = {
            openEditModal: false,
            editTaskName: null,
            item: this.props.item
        }

    }

    // handle edit of task name
    handleTaskEdit(task){
        console.log("edit task", task)
        // this.setState({openEditModal: true, editTaskName: task.title, toBeUpdatedTask: task})
        this.props.editTask(task)
    }

    // handle start task button click
    // Will push the tasks for progressTasks state
    handleStartTask(task){
        // this.setState({
        //     progressTasks: [...this.state.progressTasks, task]
        // })
    }

    // handle deletion of the task
    handleTaskDelete(taskId){
        // console.log(taskId)
        this.props.deleteTask(taskId)
        // this.props.getTasks()
    }

    componentDidUpdate(prevProps, prevState, snapshot){
        console.log("update ", prevProps, prevState)
    }

    render(){
        let {item} = this.state
        let created = Moment(item.created).format("Do MMM YYYY")
        console.log("item",item)
        return (
            <Card className="task-card" key={`task-${item.id}`}>
                <EditModal/>
                <CardContent>
                    <Typography variant="headline" component="h2">
                        {item.title}
                    </Typography>
                    <Typography color="textSecondary">
                        {created}
                    </Typography>
                </CardContent>
                <CardActions>
                    <Button size="small" color="primary" onClick={this.handleTaskEdit.bind(this, item)}>Edit</Button>
                    <Button size="small" color="primary" onClick={this.handleStartTask.bind(this, item)}>Start Task</Button>
                    <Button size="small" color="secondary" onClick={this.handleTaskDelete.bind(this, item.id)}>Delete</Button>
                </CardActions>
            </Card>
        )
    }
}

const mapStateToProps = (state) => {
    return {
    }
  }

  const mapDispatchToProps = (dispatch) => {
    return {
        getTasks: () => dispatch(TaskActions.fetchTasks()),
        deleteTask: (taskId) => dispatch(TaskActions.deleteTask(taskId)),
        editTask: (currentTask) => dispatch(TaskActions.editTask(currentTask))
    }
  }

  export default connect(mapStateToProps, mapDispatchToProps)(TaskCard)

如上图所示,卡片旁边的标题是更新的标题,由编辑按钮触发,但同样的内容不会传递给 TaskCard 组件。在屏幕截图中,有一个控制台输出,其中“update”行表明更改没有触发 componentDidUpdate。

【问题讨论】:

  • 你能把deleteTask动作代码贴出来
  • 根本不是那个问题。我已经验证过了,问题就在Container组件中调用TaskCard组件的地方。
  • 好的,你能不能尝试使用道具中的任务而不是TaskCard中的状态

标签: reactjs redux react-redux react-props react-state-management


【解决方案1】:

您的症状与您的 ListItem 上的 key 属性的问题一致。当您需要支持数据独有的东西时,看起来您正在使用数组中的索引。

在你调用 delete 之前,react 会看到一个带有键 1, 2, 3, 4 的 VDOM。然后你在3 上点击删除。现在调用 re-render 并使用 i 重新生成密钥。新密钥是1, 2, 3。 React 将以前的 DOM 与新的 DOM 进行比较,发现只有 4 丢失并将其删除。

可以通过以下两种方式之一解决您的更新问题:

  1. 在渲染中使用道具而不是状态。这是首选 选择,因为它更简单。 let {item} = this.state > let {item} = this.props(根据 Shubham 的回答)。

  2. 实现 componentDidUpdate。如果您仍想保留项目 声明您需要确保它在父组件时更新 向您的组件发送新道具:

它可能看起来像这样:

componentDidUpdate(prevProps,prevState) {
    if(this.props.item != this.state.item) {
        this.setState( { item: this.props.item });
    }
}

【讨论】:

  • 我在某处读到过,但键被分配了一个值 i,即迭代器自增。那么,我应该尝试 item.id 吗?
  • 是的,应该这样做。
  • 如果数据上没有Id,另一种方法是使用item 中数据的某些独特组合。
  • 是的,当然。只让key 指向对您的数据唯一标识的东西,切勿使用列表位置。如果该值很长,那就完美了。 data.name 或 data.title 通常是很好的候选者。
  • 我的意思是你的render()let {item} = this.state中的这一行应该是let {item} = this.props
【解决方案2】:

问题似乎是子组件中键和状态的组合。由于您使用索引作为键,如果您从中间删除项目,索引到数据渲染映射会更改并且卡片会使用不同的道具重新渲染,但是,您不会更新 taskCard 中的状态,因此数据不会不改变。设置直接从 props 派生的 state 不是正确的方法,如果你这样做,你还需要更新 state 以响应 prop 更改

要解决这个问题,你需要做的就是从状态渲染任务

 export class TaskCard extends Component{

    constructor(props){
        super(props)

        this.state = {
            openEditModal: false,
            editTaskName: null,
        }

    }

    // handle edit of task name
    handleTaskEdit(task){
        console.log("edit task", task)
        // this.setState({openEditModal: true, editTaskName: task.title, toBeUpdatedTask: task})
        this.props.editTask(task)
    }

    // handle start task button click
    // Will push the tasks for progressTasks state
    handleStartTask(task){
        // this.setState({
        //     progressTasks: [...this.state.progressTasks, task]
        // })
    }

    // handle deletion of the task
    handleTaskDelete(taskId){
        console.log(taskId)
        this.props.deleteTask(taskId)
        // this.props.getTasks()
    }


    render(){
        let {task} = this.props;
        let created = Moment(task.created).format("Do MMM YYYY")
        console.log("task", task)
        return (
            <Card className="task-card" key={task.id}>
                <EditModal/>
                <CardContent>
                    <Typography variant="headline" component="h2">
                        {task.title}
                    </Typography>
                    <Typography color="textSecondary">
                        {created}
                    </Typography>
                </CardContent>
                <CardActions>
                    <Button size="small" color="primary" onClick={this.handleTaskEdit.bind(this, task)}>Edit</Button>
                    <Button size="small" color="primary" onClick={this.handleStartTask.bind(this, task)}>Start Task</Button>
                    <Button size="small" color="secondary" onClick={this.handleTaskDelete.bind(this, task.id)}>Delete</Button>
                </CardActions>
            </Card>
        )
    }
}

但是,使用唯一的项目 ID 会提高性能。

【讨论】:

  • 谢谢,根据 jmathew 的建议,现在我正在使用 item.id 检查我的更新 2,所以,删除工作正常,但更新不起作用,好吧,它不会立即更新更新的名称。
  • @Maverick,请使用道具中的任务而不是状态。检查我的答案
  • Subham,是的,我做到了,让我将新卡片组件放入更新 3。
猜你喜欢
  • 2020-03-12
  • 1970-01-01
  • 2019-05-20
  • 1970-01-01
  • 2019-03-22
  • 1970-01-01
  • 1970-01-01
  • 2017-12-27
  • 1970-01-01
相关资源
最近更新 更多