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
})
我正在使用 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
, andshouldComponentUpdate
methods- Class component static
getDerivedStateFromProps
method- Function component bodies
- State updater functions (the first argument to
setState
)- Functions passed to
useState
,useMemo
, oruseReducer
<-- 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
})