A Parasite Problem: Setting One Piece of State After Another with the useState Hook

“To do what I do, I really need you.” -❤- Xenomorph (your favorite parasite)

Repo Example To Work With

For this blog, I’ve made a repo that you can fork, clone, and use to follow along with the examples that follow. Let’s start with the Blog Code example here, beginning with the PostsComponent:

src/components/PostsComponent.jsimport React, { useState } from 'react'
import postsData from '../data/posts'
const PostsComponent = () => {
const [posts, setPosts] = useState(postsData.posts)
const [filterNum, setFilterNum] = useState(null)
const [postsToDisplay, setPostsToDisplay] = useState(posts)
...
}

What’s happening here?

  • We set the initial value of all posts with the useState hook, setting the value to an array of posts objects imported and accessed via postsData.posts:
const [posts, setPosts] = useState(postsData.posts)
  • We set up a piece of state to filter posts that will be displayed based on whether they have enough likes:
const [filterNum, setFilterNum] = useState(null)
  • Finally, because I see this sort of thing from time to time in students’ code, we’ve set up a different slice of state for what we want displayed after taking into account what our filterNum is.
  • The initial value is all posts, drawn from the state of posts that we set up with our first useState hook. But, we will build out a rendering of posts using this slice of state to display ONLY those that meet the requirements of a filter, if there is a filterNum that has been set:
const [postsToDisplay, setPostsToDisplay] = useState(posts)

Quick Recap:

posts = all posts from the dummy data + any new posts we add to state

Adding the Likes and New Posts Features

Updating Likes

Now, let’s add in more of the code of filtering and displaying the posts, piecemeal:

src/components/PostsComponent.jsimport React, { useState } from 'react'
import { CardGroup, Container } from 'react-bootstrap'
import postsData from '../data/posts'
import Post from './Post'
const PostsComponent = () => {
const [posts, setPosts] = useState(postsData.posts)
const [filterNum, setFilterNum] = useState(null)
const [postsToDisplay, setPostsToDisplay] = useState(posts)
const renderedPosts = postsToDisplay.map(post => {
return <Post key={post.id} postInfo={post}/>
})
return(
<div>
<h1>Blog Posts</h1>
<Container>
<CardGroup>
{renderedPosts}
</CardGroup>
</Container>
</div>
}
src/components/PostsComponent.jsimport React, { useState } from 'react'
import { CardGroup, Container } from 'react-bootstrap'
import postsData from '../data/posts'
import Post from './Post'
const PostsComponent = () => {
const [posts, setPosts] = useState(postsData.posts)
const [filterNum, setFilterNum] = useState(null)
const [postsToDisplay, setPostsToDisplay] = useState(posts)
const updateLikes = (id) => {
const postToUpdate = posts.find(post => post.id === id)
const updateIndex = posts.indexOf(postToUpdate)
postToUpdate.likes += 1
setPosts(prevPosts => [...prevPosts.slice(0, updateIndex),
postToUpdate, ...prevPosts.slice(updateIndex + 1)])
}
const renderedPosts = postsToDisplay.map(post => {
return <Post
key={post.id}
postInfo={post}
updateLikes={updateLikes}
/>
})
return(
<div>
<h1>Blog Posts</h1>
<Container>
<CardGroup>
{renderedPosts}
</CardGroup>
</Container>
</div>
}

Adding New Posts

  1. Add a controlled form in a new component called PostForm, and handle data submission in our PostsComponent . As before, I will skip building out the PostForm, but you can check it out here. (You can also review my other blog on building out controlled forms with React hooks here.)
src/components/PostsComponent.jsimport React, { useState } from 'react'
import { CardGroup, Container } from 'react-bootstrap'
import postsData from '../data/posts'
import Post from './Post'
import PostForm from './PostForm'
const PostsComponent = () => {
const [posts, setPosts] = useState(postsData.posts)
const [filterNum, setFilterNum] = useState(null)
const [postsToDisplay, setPostsToDisplay] = useState(posts)
const updateLikes = (id) => {
const postToUpdate = posts.find(post => post.id === id)
const updateIndex = posts.indexOf(postToUpdate)
postToUpdate.likes += 1
setPosts(prevPosts => [...prevPosts.slice(0, updateIndex),
postToUpdate, ...prevPosts.slice(updateIndex + 1)])
}
const renderedPosts = postsToDisplay.map(post => {
return <Post
key={post.id}
postInfo={post}
updateLikes={updateLikes}
/>
})
return(
<div>
<h1>Blog Posts</h1>
<Container>
<CardGroup>
{renderedPosts}
</CardGroup>
</Container>
<br></br>
<PostForm />
</div>
}
src/components/PostsComponent.jsimport React, { useState } from 'react'
import { CardGroup, Container } from 'react-bootstrap'
import postsData from '../data/posts'
import Post from './Post'
import PostForm from './PostForm'
const PostsComponent = () => {
const [posts, setPosts] = useState(postsData.posts)
const [filterNum, setFilterNum] = useState(null)
const [postsToDisplay, setPostsToDisplay] = useState(posts)
const updateLikes = (id) => {
const postToUpdate = posts.find(post => post.id === id)
const updateIndex = posts.indexOf(postToUpdate)
postToUpdate.likes += 1
setPosts(prevPosts => [...prevPosts.slice(0, updateIndex),
postToUpdate, ...prevPosts.slice(updateIndex + 1)])
}
const renderedPosts = postsToDisplay.map(post => {
return <Post
key={post.id}
postInfo={post}
updateLikes={updateLikes}
/>
})
const handleSubmit = (newPost) => {
setPosts([...posts, newPost])
}
return(
<div>
<h1>Blog Posts</h1>
<Container>
<CardGroup>
{renderedPosts}
</CardGroup>
</Container>
<br></br>
<PostForm
postsCount={posts.length}
handleSubmit={handleSubmit}
/>

</div>
}

Adding Filter Functionality

  1. Now we come to the filtering feature, where we first want to add buttons that the user can press to select filtered blog posts, and a handleFilterNum function that sets the state of the filterNum piece of state.
src/components/PostsComponent.jsimport React, { useState } from 'react'
import { CardGroup, Container, Button } from 'react-bootstrap'
import postsData from '../data/posts'
import Post from './Post'
import PostForm from './PostForm'
const PostsComponent = () => {
const [posts, setPosts] = useState(postsData.posts)
const [filterNum, setFilterNum] = useState(null)
const [postsToDisplay, setPostsToDisplay] = useState(posts)
const updateLikes = (id) => {
const postToUpdate = posts.find(post => post.id === id)
const updateIndex = posts.indexOf(postToUpdate)
postToUpdate.likes += 1
setPosts(prevPosts => [...prevPosts.slice(0, updateIndex),
postToUpdate, ...prevPosts.slice(updateIndex + 1)])
}
const handleFilterNum = (num) => {
setFilterNum(num);
}
const renderedPosts = postsToDisplay.map(post => {
return <Post
key={post.id}
postInfo={post}
updateLikes={updateLikes}
/>
})
const handleSubmit = (newPost) => {
setPosts([...posts, newPost])
}
return(
<div>
<h1>Blog Posts</h1>
<>
<Button onClick={() => handleFilterNum(0)}>All</Button>{' '}
<Button onClick={() => handleFilterNum(5)}>5+ Likes</Button>{' '}
<Button onClick={() => handleFilterNum(10)}>10+ Likes</Button>
</>

<Container>
<CardGroup>
{renderedPosts}
</CardGroup>
</Container>
<br></br>
<PostForm
postsCount={posts.length}
handleSubmit={handleSubmit}
/>
</div>
}
  • all blogs (0 or more likes)
  • 5+ likes
  • 10+ likes
const handleFilterNum = (num) => {
setFilterNum(num);
const displayablePosts = filterNum ? posts.filter(post =>
post.likes >= filterNum
) : posts
setPostsToDisplay(displayablePosts);

}

Filtering and the Problem of Asynchronous State Changes In Series: Parasitic State

The Asynchronous Issue

UsingsetFilteNum(num) begins an asynchronous process, which likely will not be complete before we read the next line of code:

const displayablePosts = filterNum ? posts.filter(post => 
post.likes >= filterNum
) : posts

One piece of state change depends on another piece of state change. One is “parasitic” on the other.

What do I mean by parasitic here? The second state change requires the first state change to complete. It’s like a shadow. When you move around with a backlight, the shadow can only move if, and only if, you move in relation to that backlight.

At least here, Peter Pan’s shadow is doing what’s expected, without any side-effects.

A Fix using the useEffect Hook

One way to address this asynchronous issue is to make sure that the ternary and setPostsToDisplay functions fire after the filterNum piece of state has successfully been set.

src/components/PostsComponent.jsimport React, { useState, useEffect } from 'react'
import { CardGroup, Container, Button } from 'react-bootstrap'
import postsData from '../data/posts'
import Post from './Post'
import PostForm from './PostForm'
const PostsComponent = () => {
const [posts, setPosts] = useState(postsData.posts)
const [filterNum, setFilterNum] = useState(null)
const [postsToDisplay, setPostsToDisplay] = useState(posts)
const updateLikes = (id) => {
const postToUpdate = posts.find(post => post.id === id)
const updateIndex = posts.indexOf(postToUpdate)
postToUpdate.likes += 1
setPosts(prevPosts => [...prevPosts.slice(0, updateIndex),
postToUpdate, ...prevPosts.slice(updateIndex + 1)])
}
const handleFilterNum = (num) => {
setFilterNum(num);
}
const renderedPosts = postsToDisplay.map(post => {
return <Post
key={post.id}
postInfo={post}
updateLikes={updateLikes}
/>
})
useEffect(() => {
const displayablePosts = filterNum ? posts.filter(post =>
post.likes >= filterNum
) : posts
setPostsToDisplay(displayablePosts);
}, [posts, filterNum])
const handleSubmit = (newPost) => {
setPosts([...posts, newPost])
}
return(
<div>
<h1>Blog Posts</h1>
<>
<Button onClick={() => handleFilterNum(0)}>All</Button>{' '}
<Button onClick={() => handleFilterNum(5)}>5+ Likes</Button>{' '}
<Button onClick={() => handleFilterNum(10)}>10+ Likes</Button>
</>

<Container>
<CardGroup>
{renderedPosts}
</CardGroup>
</Container>
<br></br>
<PostForm
postsCount={posts.length}
handleSubmit={handleSubmit}
/>
</div>
}
  • Only then do we change the state of postsToDisplay.
useEffect(() => {
const displayablePosts = filterNum ? posts.filter(post =>
post.likes >= filterNum
) : posts
setPostsToDisplay(displayablePosts);
}, [posts, filterNum])
  • a) suppose we add a new post. We will want the postsToDisplay piece of state to update with the new post, so that it will render all of our posts, including the new one, on the page.
  • b) we want the filterNum term to be used to filter which posts we display (the scenario we’ve been working on a for a bit here).

Quick Turn-Around

You may be asking at this point:

Aren’t we still rendering out-of-date data before the useEffect rerenders?

The answer is

Yes, but don’t worry about it: it’s a quick turn-around.

You likely won’t even see the initial render before the useEffect state changes force a rerender.

A Better Way

Did we have to do all of that to get our data to display what we wanted when we add posts and filter them? In this case, “No.”

“Say no more. I know when I’m not wanted…”
src/components/PostsComponent.jsimport React, { useState } from 'react'
import { CardGroup, Container, Button } from 'react-bootstrap'
import postsData from '../data/posts'
import Post from './Post'
import PostForm from './PostForm'
const PostsComponent = () => {
const [posts, setPosts] = useState(postsData.posts)
const [filterNum, setFilterNum] = useState(null)
const updateLikes = (id) => {
const postToUpdate = posts.find(post => post.id === id)
const updateIndex = posts.indexOf(postToUpdate)
postToUpdate.likes += 1
setPosts(prevPosts => [...prevPosts.slice(0, updateIndex),
postToUpdate, ...prevPosts.slice(updateIndex + 1)])
}
const handleFilterNum = (num) => {
setFilterNum(num);
}
const displayPosts = posts.map(post => (
<Post
key={post.id}
postInfo={post}
updateLikes={updateLikes}
/>))
const displayFilteredPosts = posts.filter(post => (
post.likes >= filterNum
).map(post => (
<Post
key={post.id}
postInfo={post}
updateLikes={updateLikes}
/>)
))
const handleSubmit = (newPost) => {
setPosts([...posts, newPost])
}
return(
<div>
<h1>Blog Posts</h1>
<>
<Button onClick={() => handleFilterNum(0)}>All</Button>{' '}
<Button onClick={() => handleFilterNum(5)}>5+ Likes</Button>{' '}
<Button onClick={() => handleFilterNum(10)}>10+ Likes</Button>
</>

<Container>
<CardGroup>
{filterNum ? displayFilteredPosts : displayPosts}
</CardGroup>
</Container>
<br></br>
<PostForm
postsCount={posts.length}
handleSubmit={handleSubmit}
/>
</div>
}

Using the Ternary

Here, we just use a ternary instead of setting state for postsToDisplay:

{filterNum ? displayFilteredPosts : displayPosts}
  • If filterNum is present, then display filtered posts using the variable of displayFilteredPosts. The value of that variable is set to the output of mapping out posts that pass the filter using the filterNum.
  • If filterNum is not present (and still in an initial state of null) then we simply display all the posts.

Avoiding Parasitic State Changes

This avoids the state change that depends on another state change. In fact, we’ve entirely removed the useState hook for postsToDisplay.

Picture of an unwanted parasitic insect.
Photo by Erik Karits on Unsplash

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
David Ryan Morphew

David Ryan Morphew

10 Followers

I’m very excited to start a new career in Software Engineering. I love the languages, frameworks, and libraries I’ve already learned / worked with (Ruby, Rails,