Reputation: 463
I'm building a simple twitter clone with React and I'm facing a weird behaviour when creating a new Tweet. Right now, how it works is that I locally store a tweets array using useState and I use setState to add the new tweet into the array. According to my initial approach it works fine on the first time creating a tweet. However, on subsequent creation of tweet it is being appended twice into the array. So, all the tweets that was created after the first one will have their own respective duplicate.
Here is the screen shot of the problem
Here are the respective code:
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>
)
}
UPDATE: I managed to solve the bug (but not sure why my initial code won't work) by using the spread operator instead in the createTweetHandler function. So I am assuming it is something to do with immutability of states. Could someone explain to me why my previous code doesn't work?
Here's the updated code the apparently solved the duplicate problem:
const createTweetHandler = (newTweet) => {
console.log('Appending new tweet into state', newTweet);
setTweets((prevTweets) => {
return [...prevTweets, newTweet];
})
}
And here's the old code that was causing the issue:
const createTweetHandler = (newTweet) => {
console.log('Appending new tweet into state', newTweet);
setTweets((prevTweets) => {
console.log('setState');
prevTweets.push(newTweet);
return [...prevTweets];
})
}
Upvotes: 3
Views: 2634
Reputation: 202628
You are mutating the previous state when you push into it.
setTweets((prevTweets) => {
console.log('setState');
prevTweets.push(newTweet); // <-- mutates state!!
return [...prevTweets];
})
The reason is typically that your app is rendered into a React.StrictMode
component which invokes certain functions/lifecycle methods twice as a way to help you detect unintentional side-effects, like state mutations.
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
This means on the second invocation you are seeing the result of the mutation, i.e. the first .push
, and then a second .push
occurs and thus the net result is that two new elements were added!
As you've discovered, the fix is to not mutate the state, but to instead apply the immutable update pattern. Redux has a great explanation found here.
The idea is to create a shallow copy of the state and not effect any changes to the existing state.
setTweets((prevTweets) => {
return [
...prevTweets, // shallow copy into new array reference
newTweet, // append new element
];
})
or
setTweets((prevTweets) => {
return prevTweets.concat(newTweet); // append and return new array reference
})
Upvotes: 4