渲染组件不响应其道具更新

Rendered component doesn't respond to its props update

我正在为 Trello 创建 React 应用 "similar"。在我们有项目,项目有看板,看板有专栏,专栏有任务等等。默认情况下,只有包含带有板名的栏的项目应该是可见的——其余的应该是隐藏的。如果您单击带有板标题的栏 - 该板的全部内容应该是可见的。此外,当您再次单击此栏时,它应该会再次隐藏。我希望这很清楚。

问题是,我在一个组件 (ProjectView) 中渲染了板名称的条形图,其中包含许多 child 组件 (Board),这些组件最终可以渲染每个板的内容 (BoardView) .在 ProjectView 中,我有一张地图 (visibilityMap),它将 isHidden-boolean 分配给板的名称。这个映射的相应值被传递给每个 Board 组件的 props,Board 组件将它传递给 BoardView,它用于定义 hidden 属性值 div with content。此外,在 ProjectView 中,我有一个函数可以在单击适当的板名称后更改地图中的适当布尔值。一切都正确呈现,我可以在日志中看到,在单击 board-name 后,visibilityMap 中的特定值发生了变化,但它不会改变特定板内容的可见性。

我为此花了很多时间并尝试了很多奇怪的事情,但我不知道哪里出了问题。下面我附上了所描述组件的代码和两个屏幕截图 - 在单击 board-name.

之前和之后

ProjectView - 几乎一切都在 renderBoard

import React from 'react';
import {Button} from "react-bootstrap";
import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import Board from "../controllers/Board";
import {PhotoshopPicker} from 'react-color';

require("../../styles/Project.css");
require("../../styles/Board.css");

class ProjectView extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            render: false,
            modalShow: false,
            modalAddShow: false,
            modalBoard: "",
            boardName: "",
            boardBackground: "orange",
            newName: "",
            newBackground: "",
            showAddBackground: false,
            showChangeBackground: false,
            pickedBackground: "orange",
            validate: true,
            visibilityMap: new Map()
        };
        this.handleChange = this.handleChange.bind(this);
    }

    renderBoards = () => {
        if (this.props.boards.length === 0) {
            return (<div className="no-content">Brak board'ów!</div>)
        }

        if (this.state.visibilityMap.size === 0) {
            this.props.boards.map(board => this.state.visibilityMap.set(board.name, true))
        }

        return this.props.boards.map(board =>
            this.renderBoard(board)
        );
    };

    renderModal = (board) => {
        this.setState({
            modalShow: true,
            modalBoard: board,
            newName: board.name,
            newBackground: board.background,
        })
    };

    handleChangeColor = (color) => {
        this.setState({
            pickedBackground: color
        })
    };

    handleCancelColor = () => {
        this.setState({
            showAddBackground: false,
            showChangeBackground: false,
        })
    };

    handleAcceptAddColor = () => {
        const newColor = this.state.pickedBackground.hex;
        this.setState({
            boardBackground: newColor,
            showAddBackground: false
        })
    };

    handleAcceptChangeColor = () => {
        const newColor = this.state.pickedBackground.hex;
        this.setState({
            newBackground: newColor,
            showChangeBackground: false
        })
    };

    renderBoard = (board) => {
        const handleClose = () => {
            this.setState({
                newName: "",
                newBackground: "",
                modalShow: false
            })
        };

        const switchVisibility = () => {
            const oldValue = this.state.visibilityMap.get(board.name)
            this.state.visibilityMap.set(board.name, !oldValue)
        }

        const handleEdit = () => {
            this.props.handleEdit(this.state.modalBoard.name, this.state.newName, this.state.newBackground);
            handleClose();
            this.setState({
                modalBoard: ""
            });
        };

        return (
            <div className="board" style={{backgroundColor: board.background}}>
                <div className="bookmark board-head" onClick={switchVisibility}>
                    {board.name}
                    <Button
                        className="action-button delete"
                        id={board.name}
                        onClick={this.handleDelete}
                        variant="danger"
                    >
                        X
                    </Button>
                    <Button
                        className="action-button edit"
                        id={board.name}
                        onClick={() => this.renderModal(board)}
                        variant="warning"
                    >
                        O
                    </Button>
                    <Dialog open={this.state.modalShow} aria-labelledby="form-dialog-title">
                        <DialogTitle id="form-dialog-title">Edytuj boarda {this.state.modalBoard.name}</DialogTitle>
                        <DialogContent>
                            <TextField
                                autoFocus
                                margin="dense"
                                name="newName"
                                label="Nazwa boarda"
                                type="text"
                                onChange={this.handleChange}
                                value={this.state.newName}
                                fullWidth
                            />
                            <div style={{display: "flex", flexFlow: "nowrap row"}}>
                                <Button
                                    variant="light"
                                    onClick={() => this.setState({
                                        showChangeBackground: true,
                                        pickedBackground: this.state.newBackground
                                    })}
                                >
                                    Kolor:
                                </Button>
                                <div className="color-box"
                                     style={{background: this.state.newBackground}}>
                                </div>
                                <Dialog open={this.state.showChangeBackground} aria-labelledby="form-dialog-title">
                                    <DialogTitle id="form-dialog-title">Kolor boarda {board.name}</DialogTitle>
                                    <DialogContent>
                                        <PhotoshopPicker
                                            header="Wybierz kolor"
                                            onAccept={this.handleAcceptChangeColor}
                                            onCancel={this.handleCancelColor}
                                            color={this.state.pickedBackground}
                                            onChangeComplete={this.handleChangeColor}/>
                                    </DialogContent>
                                </Dialog>
                            </div>
                        </DialogContent>
                        <DialogActions>
                            <Button onClick={handleClose} color="primary">
                                Anuluj
                            </Button>
                            <Button
                                onClick={handleEdit}
                                disabled={this.state.newName === "" || !this.state.validate}
                                color="primary">
                                Zapisz
                            </Button>
                        </DialogActions>
                    </Dialog>
                </div>
                <Board isHidden={this.state.visibilityMap.get(board.name)} boardReference={board.ref}
                       name={board.name}/>
            </div>
        )
    };

    renderAddModal = () => {
        this.setState({
            modalAddShow: true,
        })
    };

    renderAddBoard = () => {
        const handleClose = () => {
            this.setState({
                modalAddShow: false,
                newName: ""
            })
        };

        const handleAdd = () => {
            this.props.handleSubmit(this.state.boardName, this.state.boardBackground);
            this.setState({boardName: "", boardBackground: "orange"});
            handleClose();
            this.setState({
                modalBoard: ""
            });
        };

        return (
            <div>
                <Button
                    className="add-button new-board"
                    onClick={this.renderAddModal}
                    variant="success"
                >
                    +
                </Button>
                <Dialog open={this.state.modalAddShow} aria-labelledby="form-dialog-title">
                    <DialogTitle id="form-dialog-title">Dodaj board'a</DialogTitle>
                    <DialogContent>
                        <TextField
                            autoFocus
                            margin="dense"
                            name="boardName"
                            label="Nazwa"
                            type="text"
                            onChange={this.handleChange}
                            value={this.state.boardName}
                            fullWidth
                        >
                        </TextField>
                        <div style={{display: "flex", flexFlow: "nowrap row"}}>
                            <Button
                                margin="dense"
                                fullWidth
                                variant="light"
                                onClick={() => this.setState({
                                    showAddBackground: true
                                })}
                            >
                                Kolor:
                            </Button>
                            <div className="color-box"
                                 style={{background: this.state.boardBackground}}>
                            </div>
                        </div>
                        <Dialog open={this.state.showAddBackground} aria-labelledby="form-dialog-title">
                            <DialogTitle id="form-dialog-title">Kolor nowego boarda</DialogTitle>
                            <DialogContent>
                                <PhotoshopPicker
                                    header="Wybierz kolor"
                                    onAccept={this.handleAcceptAddColor}
                                    onCancel={this.handleCancelColor}
                                    color={this.state.pickedBackground}
                                    onChangeComplete={this.handleChangeColor}/>
                            </DialogContent>
                        </Dialog>
                    </DialogContent>
                    <DialogActions>
                        <Button onClick={handleClose} color="primary">
                            Anuluj
                        </Button>
                        <Button onClick={handleAdd}
                                disabled={this.state.boardName === "" || !this.state.validate}
                                color="primary">
                            Dodaj
                        </Button>
                    </DialogActions>
                </Dialog>
            </div>
        );
    };

    handleDelete = e => {
        e.preventDefault();
        this.props.handleDelete(e.target.id);
    };

    handleChange = e => {
        e.preventDefault();
        const isValid = this.validator(e.target)
        this.setState({
            [e.target.name]: e.target.value,
            validate: isValid
        });
    };

    validator = input => {
        if (input.name === "newName" && this.state.modalBoard.name === input.value)
            return true
        const filter = this.props.boards.find(board =>
            board.name === input.value
        )
        return typeof (filter) === "undefined"
    }

    componentDidMount() {
        setTimeout(function () {
            this.setState({render: true})
        }.bind(this), 1000)
    }

    render() {
        let renderContainer = false;
        if (this.state.render) {
            renderContainer =
                <div className="project-body">
                    {this.renderBoards()}
                    {this.renderAddBoard()}
                </div>
        }
        return (
            renderContainer
        )
    }
}

export default ProjectView;

Board - 仅从道具中获取 isHidden 并将其传递给 BoardView 道具

import React from 'react';
import BoardView from '../views/BoardView';
import {ColumnService} from '../../services/ColumnService';

class Board extends React.Component {
    constructor(props) {
        super(props)
        this.boardReference = this.props.boardReference;
        this.columnService = new ColumnService();
        this.state = {
            columns: [],
            isHidden: this.props.isHidden
        }
    }

    componentDidMount() {
        this.setDatabaseListener();
    }

    handleSubmit = (columnName, order) => {
        this.columnService.addColumn(columnName, order, this.boardReference);
    }

    handleEdit = (name, newColumnName, newColumnOrder) => {
        this.columnService.editColumn(name, newColumnName, newColumnOrder, this.boardReference);
    }

    handleDelete = data => {
        const name = data.id;
        this.columnService.deleteColumn(name, this.boardReference);
    }

    render() {
        return (
            <BoardView
                isHidden={this.state.isHidden}
                columns={this.state.columns}
                handleSubmit={this.handleSubmit}
                handleEdit={this.handleEdit}
                handleDelete={this.handleDelete}
            />
        )
    }

    setDatabaseListener() {
        this.columnService.columnRef(this.boardReference).onSnapshot(data => {
            const listOfFetchedColumns = [];
            data.docs.forEach(doc => {
                const columnReference = doc.ref;
                const data = doc.data();
                data['ref'] = columnReference;
                listOfFetchedColumns.push(data);
                console.log('fetched columns', data);
            });
            listOfFetchedColumns.sort((a, b) => (a.order > b.order) ? 1 : -1)
            this.setState({
                columns: listOfFetchedColumns
            });
        });
    }
}

export default Board;

BoardView - 最后最重要,在 render

import React from 'react';
import {Button} from "react-bootstrap";
import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import Column from "../controllers/Column";

require("../../styles/Board.css");
require("../../styles/Column.css");

class BoardView extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            render: false,
            modalShow: false,
            modalAddShow: false,
            modalColumn: "",
            columnName: "",
            columnOrder: "",
            newName: "",
            newOrder: "",
            validate: true,
            hideColumns: true,
            isHidden: this.props.isHidden
        };
        this.handleChange = this.handleChange.bind(this);
    }

    renderColumns = () => {
        if (this.props.columns.length === 0) {
            return (<div className="no-content">Brak kolumn!</div>)
        }

        return this.props.columns.map(column =>
            this.renderColumn(column)
        );
    };

    renderModal = (column) => {
        this.setState({
            modalShow: true,
            modalColumn: column,
            newName: column.name,
            newOrder: column.order
        })
    };

    renderColumn = (column) => {
        const handleClose = () => {
            this.setState({
                modalShow: false,
                newName: "",
                newOrder: ""
            })
        };

        const handleEdit = () => {
            handleClose();
            this.props.handleEdit(this.state.modalColumn.name, this.state.newName, parseInt(this.state.newOrder));
            this.setState({
                modalColumn: ""
            });
        };

        return (
            <div className="column">
                <div className="bookmark column-head">
                    {column.name}
                    <Button
                        className="action-button delete"
                        id={column.name}
                        onClick={this.handleDelete}
                        variant="danger"
                    >
                        X
                    </Button>
                    <Button
                        className="action-button edit"
                        id={column.name}
                        onClick={() => this.renderModal(column)}
                        variant="warning"
                    >
                        O
                    </Button>
                    <Dialog open={this.state.modalShow} aria-labelledby="form-dialog-title">
                        <DialogTitle id="form-dialog-title">Edytuj kolumnę {this.state.modalColumn.name}</DialogTitle>
                        <DialogContent>
                            <TextField
                                autoFocus
                                margin="dense"
                                name="newName"
                                label="Nazwa kolumny"
                                type="text"
                                onChange={this.handleChange}
                                value={this.state.newName}
                                fullWidth
                            />
                            <TextField
                                autoFocus
                                margin="dense"
                                name="newOrder"
                                label="Kolejność kolumny"
                                type="number"
                                min={1}
                                onChange={this.handleChange}
                                value={this.state.newOrder}
                                fullWidth
                            />
                        </DialogContent>
                        <DialogActions>
                            <Button onClick={handleClose} color="primary">
                                Anuluj
                            </Button>
                            <Button onClick={handleEdit}
                                    disabled={
                                        this.state.newName === "" ||
                                        this.state.newOrder === "" ||
                                        this.state.newOrder < 0 ||
                                        !this.state.validate
                                    }
                                    color="primary">
                                Zapisz
                            </Button>
                        </DialogActions>
                    </Dialog>
                </div>
                <Column columnReference={column.ref} name={column.name}/>
            </div>
        )
    };

    renderAddModal = () => {
        this.setState({
            modalAddShow: true,
        })
    };

    renderAddColumn = () => {
        const handleClose = () => {
            this.setState({
                modalAddShow: false,
                newName: ""
            })
        };

        const handleAdd = () => {
            this.props.handleSubmit(this.state.columnName, parseInt(this.state.columnOrder));
            this.setState({columnName: "", columnOrder: ""});
            handleClose();
            this.setState({
                modalColumn: ""
            });
        };

        return (
            <div className="new-column-button-wrapper">
                <Button
                    className="add-button new-column"
                    onClick={this.renderAddModal}
                    variant="success"
                >
                    +
                </Button>
                <Dialog open={this.state.modalAddShow} aria-labelledby="form-dialog-title">
                    <DialogTitle id="form-dialog-title">Dodaj kolumnę</DialogTitle>
                    <DialogContent>
                        <TextField
                            autoFocus
                            margin="dense"
                            name="columnName"
                            label="Nazwa kolumny"
                            type="text"
                            onChange={this.handleChange}
                            value={this.state.columnName}
                            fullWidth
                        >
                        </TextField>
                        <TextField
                            margin="dense"
                            name="columnOrder"
                            label="Kolejność"
                            type="number"
                            min={1}
                            onChange={this.handleChange}
                            value={this.state.columnOrder}
                            fullWidth
                        >
                        </TextField>
                    </DialogContent>
                    <DialogActions>
                        <Button onClick={handleClose} color="primary">
                            Anuluj
                        </Button>
                        <Button onClick={handleAdd} disabled={
                            this.state.columnName === "" ||
                            this.state.columnOrder === "" ||
                            this.state.columnOrder < 0 ||
                            !this.state.validate
                        } color="primary">
                            Dodaj
                        </Button>
                    </DialogActions>
                </Dialog>
            </div>
        );
    };

    handleDelete = e => {
        e.preventDefault();
        this.props.handleDelete(e.target);
    };

    handleChange = e => {
        e.preventDefault();
        const isValid = this.validator(e.target)
        this.setState({
            [e.target.name]: e.target.value,
            validate: isValid
        });
    };

    validator = input => {
        if (input.name === "newName" && this.state.modalColumn.name === input.value)
            return true
        const filter = this.props.columns.find(column =>
            column.name === input.value
        )
        return typeof (filter) === "undefined"
    }

    componentDidMount() {
        setTimeout(function () {
            this.setState({render: true})
        }.bind(this), 1000)
    }

    render() {
        let renderContainer = false;
        if (this.state.render) {
            renderContainer =
                <div className="board-body" hidden={this.state.isHidden}>
                    {this.renderColumns()}
                    {this.renderAddColumn()}
                </div>
        }
        return (
            renderContainer
        )
    }

}

export default BoardView;

Board content is visible - click on board-name to hide it...

Board content is hidden - click on board-name to show it...

感谢您的帮助!

我的猜测是,因为您是直接使用地图 setter 更改 visibilityMap 上的 属性(在您的 switchVisibility 方法中)而不是使用 setState 进行设置,所以不会重新渲染。 React 使用 setState 来知道何时检查状态变化,这会导致重新渲染。

所以道具正在改变,因为你确实改变了它,但 React 没有重新渲染,因为它从来没有注意到状态被改变了。

我在使用 javaScript 时从未使用过 Map,但我假设您选择了 Map 而不是对象是因为您想按顺序索引它?

如果是这样,我猜您需要在 switchVisibility 方法中执行类似的操作:

this.setState(state => {
let oldValue = state.visibilityMap.get(board.name)
state.visibilityMap.set(board.name, !oldValue)
return state
 )};

这将更新状态对象以包含新的 visibilityMap 和更新的道具,然后 React 应该重新渲染。

如果这对您来说很神奇,setState 方法,当您传递给它时,回调会自动将当前状态作为参数传递给该 callBack。然后您可以再次操作它和 return 一个对象来更新状态。用于切换事物,或者当您需要当前状态以了解如何更新新状态时。