在我的 "useState" 不断更新时作出反应 re-render

React re-render when my "useState" constant updates

我开始学习 React,更具体地说是 ReactJS,现在我有了 NodeJS 后端,我在其中存储了一个项目数组,其中包含:id(uuid),url (字符串),技术(字符串数组),标题(字符串)和喜欢(数字)。

我的组件正在为我的后端发送到前端的每个项目渲染一个 div,每个 div 将包含该项目的所有属性,还有一个按钮在我的后端触发一条路线来增加喜欢的数量。它应该在单击按钮的同时更新该数字,我想这样做而不需要再次向 API 请求所有项目列表。

嗯,我的组件是这样的:

import React, { useContext, useEffect, useState } from 'react';
import { ThemeContext } from '../../providers/ThemeContext';
import { Button } from '@material-ui/core';
import axios from '../../services/axios';

function ListProjects() {
    const [projects, setProjects] = useState<any[]>([]);
    const { theme } = useContext(ThemeContext);

    const updateProjects = async () => {
        const res = await axios.get('list');
        res.data.status === 1 && setProjects(res.data.projects);
    };

    useEffect(() => {
        updateProjects();
    }, []);

    const onLikeProject = async (id: string) => {
        const newObj = (await axios.post(`${id}/like`)).data.updatedProject;
        const objIndex = projects.findIndex(i => i.id === id);
    
        let projectsCopy = projects;
        /* console.log(projects[objIndex]); */
        projectsCopy[objIndex] = newObj;
        /* console.log(projectsCopy[objIndex]); */
        setProjects(projectsCopy);
        /* console.log(projects[objIndex]); */
    };

    return (
        <div style={{ 
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            minHeight: 'calc(100vh - 64px)', 
            minWidth: '100vw', 
            backgroundColor: theme.bg, 
            color: theme.color
        }}>
            { projects && projects.map((p, i) => <div key={p.id} style={{
                display: 'flex',
                flexDirection: 'column',
                width: '550px',
                height: '300px',
                border: '1px solid blue',
                marginBottom: '10px'
            }}>
                <h3>Title: { p.title }</h3>
                <h3>URL: { p.url }</h3>
                <h3>Techs: { p.techs.map((t: any, i: any) =>  i+1 < p.techs.length ? `${t}, ` : t) }</h3>
                <h3>Likes: { p.likes }</h3>
                <Button color="primary" variant="contained" onClick={() => onLikeProject(p.id) }>Like</Button>
            </div>) }
        </div>
    );
}

export default ListProjects;

如您所见,当页面加载时,项目状态会根据 API 列表响应进行更新。我已经尝试使用相同的 updateProjects() 函数在用户单击 Like 按钮时更新列表,但这会触发另一个 api 调用,这会延迟一点,并使 api 列出每个类似请求的请求,将打破服务可用请求的可能限制。所以我尝试了 onLikeProject() 中的代码,但它不会更新 UI,即使正在更新项目数组。

我该如何进行? 谢谢

问题

状态突变。 projectsCopy 是对状态对象 projects 的引用,它会发生变异并保存回状态,因此状态数组引用永远不会更新,React 会在任何重新渲染时退出。

const onLikeProject = async (id: string) => {
    const newObj = (await axios.post(`${id}/like`)).data.updatedProject;
    const objIndex = projects.findIndex(i => i.id === id);

    let projectsCopy = projects;
    projectsCopy[objIndex] = newObj; // <-- state mutation
    setProjects(projectsCopy);
};

解决方案

将状态对象浅复制到新的数组引用中。

const onLikeProject = async (id: string) => {
  const newObj = (await axios.post(`${id}/like`)).data.updatedProject;
  const objIndex = projects.findIndex(i => i.id === id);

  let projectsCopy = [...projects]; // <-- shallow copy state array
  projectsCopy[objIndex] = newObj; // <-- then update index
  setProjects(projectsCopy);
};

更规范的方法是将前一个状态映射到下一个状态。

const onLikeProject = async (id: string) => {
  const newObj = (await axios.post(`${id}/like`)).data.updatedProject;
    
  setProjects(projects => projects.map((project) => project.id === id ? newObj : project));
};