React:自定义组件不代表正确的数据

React: Custom component not representing the right data

我现在面临一个奇怪的问题。 我有一个通过休息 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)

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

更新 1

所以,我试着从我这边调试它。看起来 TaskCard 在某种程度上缓存了道具。

在添加了 {item.title} 的图像中和正下方是呈现任务卡的位置,两者的标题不同,但 {item.title} 是正确的。

更新 2

基于jmathew,answer,我更新了ListItem和TaskCard的key为item.id,所以delete可以,但是update还是不行。同样,根据更新 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。

您的症状与 ListItemkey 道具的问题一致。当您需要支持数据独有的内容时,您似乎正在使用数组中的索引。

在你调用 delete 之前,react 会看到一个带有键 1, 2, 3, 4 的 VDOM。然后你在 3 上点击删除。现在重新渲染被调用,你使用 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 });
    }
}

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

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

 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 将提高性能。