Table of Contents:
- React data flow
- Definition of state
- Deciding where to hold state
- Setting up state
- Updating state
- Working demo
I've spent the last few weeks getting a handle on React and can already see how much easier my life as a developer will be because of it. While there are myriad topics I could delve into regarding React, today I'll focus on one of the most important - state. If you already have a general understanding of React components (read here if not), this post will provide an introduction on how to take advantage of the power of state in your application. I will also be using destructuring throughout my code snippets.
React's data flow
React's data flow runs one way down the component hierarchy, from Parent to Child component. Props are pieces of data that are passed from Parent to Child component. For example, say you have an App
that contains two child components- SongTitles
and SongArtists
. App
might have a songs
array of objects representing each song, where keys are the song titles and values are the corresponding artists. If we want to render the titles and artists in their respective components, we might want to pass that information as props
to SongTitles
and SongArtists
. This might not be the most elegant design, but here's a snippet of what our components could look like:
//App.js
function App() {
const songs = [
{ "Blank Space": "Taylor Swift" },
{ "Tiffany Blews": "Fall Out Boy" },
{ "Knee Socks": "Arctic Monkeys" },
];
const titles = songs.map((song) => Object.keys(song)[0]);
const artists = songs.map((song) => Object.values(song)[0]);
return (
<div className="App">
<h1>React State Demo</h1>
<SongTitles titles={titles} />
<SongArtists artists={artists} />
</div>
);
}
//SongTitles.js
function SongTitles({ titles }) {
const songList = titles.map((title) => (
<p key={title}>
<button>X</button> {title}
</p>
));
return (
<div className="titles">
<h2>Song Titles</h2>
{songList}
</div>
);
}
//SongArtists.js
function SongArtists({ artists }) {
const artistList = artists.map((artist) => <p key={artist}>{artist}</p>);
return (
<div className="artists">
<h2>Song Artists</h2>
{artistList}
</div>
);
}
Here's what the App looks like:
Definition of state
This looks good, but what happens if we want to change the songs
object dynamically and remove a song from the screen when we click a delete button? Here's where state comes in. State data can change throughout a component's lifetime as the user interacts with the app, unlike props. If we want to be able to delete a song (and its corresponding artist) from inside the SongTitles
component, we can easily do this by setting state. If we make the songs
array stateful rather than static, it will update dynamically and we won't have to refresh the page each time we want to render a change.
Deciding where to hold state
Before we create our state variables, we should figure out where to hold songs
's state. To figure out where state should live, it's helpful to decide which components need access to that state. In this case, SongTitles
and SongArtists
both need access to songs
to render information. Sibling components can't pass data to each other (remember the data flow) so we should store songs
's state in a common parent component (App
in this case). This means that songs
can continue living in App
, but you can imagine that without the SongArtists
component, we could just store this state directly in SongTitles
.
Setting up state
Now that we've decided where the songs
state will live, let's import React's useState
hook in App.js: import {useState} from "react"
The useState
function takes in the initial value of our state variable as a parameter and returns two things:
1) the state's current value
2) a function that can be used to set (or change) that state
To use state with our songs
array, we can replace our old array with the following lines inside of our App
function:
// App.js
function App() {
const [songs, setSongs] = useState([
{ "Blank Space": "Taylor Swift" },
{ "Tiffany Blews": "Fall Out Boy" },
{ "Knee Socks": "Arctic Monkeys" },
]);
//...
}
Here, songs
is initially set to the the same 3 songs from our initial array, but we also now have a function to dynamically change songs
called setSongs
.
NOTE: This is the naming convention for setter functions, but you could name it something other than
setSongs
if you prefer.
Updating state
Our next step is to create a function in App
that will remove a song from songs
. Given a song title that we want to delete, our function could look like this:
// App.js
function App() {
//...
function handleDeleteSong(title){
const updatedSongs = songs.filter(song=>!(title in song));
}
//...
}
Right now this function is creating a new array of songs. It contains all of our original songs except for the one with the title we want to delete. Your first thought might be to just set songs
equal to updatedSongs
. However, because we want to modify an internal state living in App
, this is where our setter function comes in handy.
// App.js
function App() {
//...
function handleDeleteSong(title){
const updatedSongs = songs.filter(song=>!(title in song));
setSongs(updatedSongs); //Updating songs state using our new songs array
}
//...
}
We are using setSongs
to update our internal state to a new array of songs, with our deleted song removed.
We're not quite done yet! We haven't actually called this function anywhere. We want handleDeleteSong
to run when we click the delete button next to a song's title. We'll need to access this function from within SongTitles
and use it as a callback in a click event listener. We can pass a reference to handleDeleteSong
as a prop down to SongTitles
. I'm going to call this prop onDeleteSong
.
// App.js
function App() {
//...
return (
<div className="App">
<h1>React State Demo</h1>
<SongTitles titles={titles} onDeleteSong={handleDeleteSong}/>
<SongArtists artists={artists} />
</div>
);
}
Now within SongTitles
, we can call onDeleteSong
every time we click a delete button.
//SongTitles.js
function SongTitles({ titles, onDeleteSong }) {
//Added onDeleteSong prop
const songList = titles.map((title) => (
<p key={title}>
{/* Adding click event listener below, passing in title we want to delete */}
<button onClick={(e) => onDeleteSong(title)}>X</button> {title}
</p>
));
return (
<div className="titles">
<h2>Song Titles</h2>
{songList}
</div>
);
}
This inverses the data flow and triggers a state change in App
from its child component SongTitles
. This is generally a good strategy to use when you want to update a parent's state from a child.
One great thing about React is that it automatically re-renders a component when its state changes. This means that when we click a delete button, App
and its children, SongTitles
and SongArtists
, will re-render with the updated state.
NOTE: This works because
updatedSongs
from ourhandleDeleteSong
function is an entirely new array. Because we used thefilter
method,updatedSongs
points to a different place in memory thansongs
. React does not re-render a component if you attempt to set state with a shallowly unchanged object. If we tried to set state by directly modifyingsongs
(e.g. by using splice() to remove a song fromsongs
),setSongs(songs)
would be unsuccessful. Even if we successfully removed an element fromsongs
this way, state would not update and therefore our screen would not be updated to reflect our changes.
Here's a quick demo of our working app:
If you'd like to see the source code for our 3 components, you can check out this github repository.
You can read more about state here.
Top comments (0)