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

David Ryan Morphew
10 min readOct 11, 2021

--

Parasitic State Changes and Rendering

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

With React Hooks like useState, you can work with different pieces of state in slices. This can be both wonderful OR a big headache requiring extra code, if you do not structure your state handling with useState efficiently. (I previously wrote a blog on one way to handle a controlled form with the useState hook that minimizes the duplication of functions for handling change in the form.)

Here I want to address what I would like to call “Parasitic State Changes.”

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)
...
}

Quick Note: for this simple repo, I’ve put some dummy data in src/data/posts which is imported into the PostsComponent file.

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

filterNum = filter number that we can use to filter out which posts we want displayed

postsToDisplay = only posts that meet the filter requirement, which we will then map out for display

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:

1.) Add a Post Component to display in the PostsComponent:

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>
}

If you want to look at the Post component itself, you can go here. I’ll skip going through the display at the moment. Know that renderedPosts will produce Post components that display the title, text body, and number of likes (with an initial value of 0) for each blog post.

2.) Add functionality to update likes to Posts:

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>
}

Here, we build the updateLikes arrow function and pass it down to the Post component as a prop. In the Post component (not shown here), the user can click the like button, and that will fire the updateLikes function, passing in the liked post’s id.

In the PostsComponent, the state for the post will be updated after finding the post, its index in the array of posts, and then spreading out the previous state of the posts array to change the like count of only the particular post whose id matches the one passed up from the Post component.

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>
}

2. Add handleSubmit to add a new post to the posts in our state:

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>
}

Here, we spread the previous state of posts and add the new post to the end at form submission, passing down the function to the PostForm component as a prop.

We also pass down the current number of posts as a prop called postsCount, which will allow us to set the new post’s id as the next integer after the last post’s id, which is already set in our posts slice of state.

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>
}

This is all pretty arbitrary, but I set up these buttons and filters along the following lines:

  • all blogs (0 or more likes)
  • 5+ likes
  • 10+ likes

2. Filtering Posts with the filterNum piece of state…we hit a snag!

const handleFilterNum = (num) => {
setFilterNum(num);
const displayablePosts = filterNum ? posts.filter(post =>
post.likes >= filterNum
) : posts
setPostsToDisplay(displayablePosts);

}

If we try to set the state for postsToDisplay immediately after setting the state for filterNum, we hit a problem. But why?

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

The ternary in use here asks if there is a filterNum piece of state present. If the setFilterNum process is not complete, and the filterNum piece of state has not been set anew before we reach this line of code, then the ternary will be using an out-of-date piece of state (i.e. filterNum) to determine whether or not to update the postsToDisplay piece of state.

This is bad. The posts rendered will not represent the user’s choice of filter.

One piece of state, the postsToDisplay, cannot successfully change until another piece of state, the filterNum, has successfully changed. In other words:

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.

We can do this by allowing the PostsComponent to finish rendering, which will mean that filterNum has successfully been set, and then cause a rerender with the useEffect hook:

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>
}

The useEffect hook will run immediately after the PostsComponentrenders. We can be sure that the setFilterNum asynchronous process is complete and filterNum now represents the user’s selection.

  • Only then do we change the state of postsToDisplay.

We also set the dependencies (the second argument in the useEffect callback) to watch for changes inposts and filterNum to make sure that the useEffect hook will come into effect if either of those pieces of state is changed.

useEffect(() => {
const displayablePosts = filterNum ? posts.filter(post =>
post.likes >= filterNum
) : posts
setPostsToDisplay(displayablePosts);
}, [posts, filterNum])

This is important for two scenarios:

  • 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.”

Here, it would have been better not to create a separate piece of state of postsToDisplay at all.

We then can entirely avoid the “Parasitic State Problem.”

“Say no more. I know when I’m not wanted…”

If you’re looking at the repo, here’s where you can switch the code by commenting out the “parasitic option” code and commenting in the “better option” code.

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.

In doing so, we’ve eliminated the parasites.

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

That, undoubtedly, is better.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

David Ryan Morphew
David Ryan Morphew

Written by David Ryan Morphew

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,

No responses yet

Write a response