React setState 数组在第二次调用时两次附加相同的项目

React setState array is appending the same item twice on second call onward

我正在使用 React 构建一个简单的 Twitter 克隆,但在创建新推文时我遇到了一个奇怪的行为。现在,它的工作原理是我使用 useState 在本地存储推文数组,然后使用 setState 将新推文添加到数组中。根据我最初的方法,它在第一次创建推文时运行良好。但是,在随后创建推文时,它被两次附加到数组中。因此,在第一个推文之后创建的所有推文都将有自己的副本。

这是问题的屏幕截图

这里是相应的代码:

Home.js

import React, { useState } from "react";
import { MainContentWrapper, StyledHeader } from "../style";
import { Tweet } from "../Tweet";
import { CreateTweet } from "./CreateTweet";

function Home() {
  const [ tweets, setTweets ] = useState([{
    id: 1,
    content: 'Alyssa\'s First Tweet!',
    createdAt: '2021-10-28T21:33:41.453Z',
    user: {
      username: 'aly',
      name: 'Alyssa Holmes',
    }
  }, {
    id: 2, 
    content: 'Hello Twitter Clone!',
    createdAt: '2021-10-28T21:33:41.453Z',
    user: {
      username: 'martinxz',
      name: 'Martin San Diego'
    }
  }, {
    id: 3,
    content: 'Going to starbucks today :D',
    user: {
      username: 'rickyyy',
      name: 'Rick & Morty'
    }
  }]);
  
  const createTweetHandler = (newTweet) => {
    console.log('Appending new tweet into state', newTweet); 
    setTweets((prevTweets) => {
      console.log('setState');
      prevTweets.push(newTweet);
      return [...prevTweets];
    })
  }

  return (
    <MainContentWrapper>
      <StyledHeader>
        <h3>Home</h3>
      </StyledHeader>
    <CreateTweet onCreateTweet={createTweetHandler}/>
      { tweets.map((tweet) => {
        return <Tweet key={tweet.id} tweet={tweet} />
      }) }
    </MainContentWrapper>
  );
}

export default Home;

CreateTweet.js

import React from 'react';
import { useForm } from "react-hook-form";
import { useSelector } from 'react-redux';
import { authUser } from '../../authentication/authenticationSlice';
import styled from 'styled-components/macro';
import avatarImg from "../../../assets/images/avatar_placeholder.jpg";
import { Button } from '../../../shared/Button.styled';
import { MainContentWrapper, StyledAvatar,StyledText } from '../style';

const CreateTweetWrapper = styled(MainContentWrapper)`
  & form {
    padding: 10px 20px;
    margin: 0 0 5px 0;
    display: grid;
    grid-template-columns: min-content 1fr;
    grid-gap: 5px 20px;
    align-items: center;
    border-width: 0 0 1px 0;
  }

`

const StyledInput = styled.textarea`
  background-color: ${({ theme }) => theme.colors.background};
  color: ${({ theme }) => theme.colors.text};
  border: none;
  font-size: 20px;
  resize: none;
  &:focus {
    outline: none;
  }
`;

const ButtonWrapper = styled.div`
  grid-column: 2;
  display:flex;
  justify-content: space-between;
  align-items: center;
`;

const StyledButton = styled(Button)`
  justify-self: end;
  height: 35px;
  padding: 0 15px;
`

export function CreateTweet({onCreateTweet}) {
  const user = useSelector(authUser);
  const { register, handleSubmit } = useForm({
    defaultValues: {
      content: "",
    },
  });
  const onSubmit = (data) => {
    const newTweet = {
      id: Math.floor(Math.random() * 10000),
      content: data.content,
      user: {
        username: user.username,
        name: user.attributes.name
      }
    }
    console.log('happens here');
    onCreateTweet(newTweet);
  }
 
  return (
    <CreateTweetWrapper>
      <form>
        <StyledAvatar src={`${avatarImg}`}/>
        <StyledInput rows="1" placeholder="What's happening?" {...register("content")} />
        <ButtonWrapper>
          <StyledText> Icons </StyledText>
          <StyledButton buttonType="secondary" type="submit" onClick={handleSubmit(onSubmit)}>Tweet</StyledButton>
        </ButtonWrapper>
      </form>
    </CreateTweetWrapper>
  )
}

更新:我设法通过在 createTweetHandler 函数中使用扩展运算符来解决这个错误(但不确定为什么我的初始代码不起作用)。所以我假设这与状态的不变性有关。有人可以向我解释为什么我以前的代码不起作用吗?

这是显然解决了重复问题的更新代码:

  const createTweetHandler = (newTweet) => {
    console.log('Appending new tweet into state', newTweet); 
    setTweets((prevTweets) => {
      return [...prevTweets, newTweet];
    })
  }

这是导致问题的旧代码:

  const createTweetHandler = (newTweet) => {
    console.log('Appending new tweet into state', newTweet); 
    setTweets((prevTweets) => {
      console.log('setState');
      prevTweets.push(newTweet);
      return [...prevTweets];
    })
  }

问题

当你推入之前的状态时,你正在改变它。

setTweets((prevTweets) => {
  console.log('setState');
  prevTweets.push(newTweet); // <-- mutates state!!
  return [...prevTweets];
})

原因通常是您的应用程序被渲染到 React.StrictMode 组件中,该组件调用 某些 functions/lifecycle 方法两次,以帮助您检测无意的副作用,如状态突变。

Detecting unexpected side effects

Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:

  • Class component constructor, render, and shouldComponentUpdate methods
  • Class component static getDerivedStateFromProps method
  • Function component bodies
  • State updater functions (the first argument to setState)
  • Functions passed to useState, useMemo, or useReducer <-- this

这意味着在第二次调用时,您将看到突变的结果,即第一个 .push,然后是第二个 .push,因此最终结果是 添加了两个个新元素!

解决方案

如您所见,解决方法是不是改变状态,而是应用不可变更新模式。 Redux 找到了很好的解释 here.

想法是创建状态的浅表副本,而不影响现有状态的任何更改。

setTweets((prevTweets) => {
  return [
    ...prevTweets, // shallow copy into new array reference
    newTweet,      // append new element
  ];
})

setTweets((prevTweets) => {
  return prevTweets.concat(newTweet); // append and return new array reference
})