React, State, and You
Darren
Posted on October 21, 2020
React can be daunting to start off with, but once you learn some basic concepts even the most complicated of interactions can be simple. The thing to remember is you can only really influence three basic things
- Children
- Props
- State
So let's learn them.
Children
All React apps start from a single component, which is sometimes called the 'entry point' for your application:
ReactDOM.render(<App />, rootElement);
Here, <App />
is our 'entry point'. It's the component from which any other component in our application needs to be a child of. We can then define <App />
to be something like this:
export default function App() {
return (
<Child>
<GrandChild />
</Child>
);
}
The structure is <App> -> <Child> -> <GrandChild>
with each being a 'child' of it's parent.
Props
The second fundamental concept is the properties that you provide to your components. Properties, are variables that the parent wants to share with a child. The child can then make use of these properties - shortened to props
. Properties are defined to a child, and subsequently consumed by a child as follows:
const Child = (props) => (
<p>Hi, my name is {props.name} </p>
);
export default function App() {
return (
<div>
<Child name="Billy" />
<Child name="Mary" />
<Child name="Colin" />
</div>
);
}
Here, each child is provided a different value for 'name' and the Child
component itself uses the name provided in the props
object. When using functional components like this, props
is always the first argument to your function, if you were using class Child extends React.Component
you would need to use this.props
but otherwise it works the same. Props can contain anything you want, the only preset prop is children
which are 'children' provided by the parent, for example:
const Child = (props) => (
<div>
<p>Hey I am a child</p>
{props.children && <div>And I have my own children {props.children}</div>}
</div>
);
const GrandChild = () => <p>Hey I am a grandchild!</p>;
export default function App() {
return (
<Child>
<GrandChild />
</Child>
);
}
A <GrandChild>
is being provided to the <Child>
by the <App>
. This will be accessible to the <Child>
using props.children
. You can see in the Child
function that we are checking if props.children
is set and if it is we are rendering them in the <div>
component.
State
So we have seen what a parent
can provide to a child
through props
, but what if the child itself wants to maintain some data of it's own. This is where 'state' comes in, and effectively state is a variable that lives inside of a component and exists during the lifetime of that component. There are some differences between 'functional' and 'class' based components, here I will be talking exclusively about the 'functional' format for state management. Let's look at a really simple example:
const names = ['Mary', 'Bill', 'Fred', 'Juan']
export default function App() {
return (
<div>
<h1>Today is who's birthday?</h1>
<ul>
{names.map((name) => <li>{name}</li>)}
</ul>
</div>
);
}
Here we have a simple array of names, which our component is then rendering into a list. We want to maintain 'state' of who's birthday it actually is. One really simple way would be to include a variable as follows:
const names = ["Mary", "Bill", "Fred", "Juan"];
export default function App() {
const birthdayPerson = "Bill";
return (
<div>
<h1>Today is who's birthday?</h1>
<ul>
{names.map((name) => (
<li>{`${name}${
name === birthdayPerson ? " HAPPY BIRTHDAY!!!" : ""
}`}</li>
))}
</ul>
</div>
);
}
We can change the birthdayPerson
to equal anyone from the list, but currently it's hardcoded. What if we want the user to be able to click one of the names in the list, thereby setting the birthdayPerson
to that person. Sadly we can't just create our own variable and update it because React works by re-rendering changes when it detects a change - so it needs help detecting those changes. So the following won't work:
const names = ["Mary", "Bill", "Fred", "Juan"];
export default function App() {
let birthdayPerson = "Bill";
return (
<div>
<h1>Today is who's birthday?</h1>
<ul>
{names.map((name) => (
// this won't work!
<li onClick={() => (birthdayPerson = name)}>{`${name}${
name === birthdayPerson ? " HAPPY BIRTHDAY!!!" : ""
}`}</li>
))}
</ul>
</div>
);
}
Instead, we need to use the useState
hook. useState
is a 'hook' function built into React that allows us to declare a variable and get a function that allows us to change this variable. This way React knows when the variable has changed so can compute the new render and decide what needs to update efficiently.
import React, {useState} from 'react';
const names = ["Mary", "Bill", "Fred", "Juan"];
export default function App() {
const [birthdayPerson, setBirthdayPerson] = useState("Fred");
return (
<div>
<h1>Today is who's birthday?</h1>
<ul>
{names.map((name) => (
<li onClick={() => setBirthdayPerson(name)}>{`${name}${
name === birthdayPerson ? " HAPPY BIRTHDAY!!!" : ""
}`}</li>
))}
</ul>
</div>
);
}
Always remember to import useState
when you want to use it. useState
is a function that will provide your component with an array. The first thing in the array is the current value for the state, with the argument passed into useState(arg)
being the initial
state (in the above case birthdayPerson === "Fred"
). The second thing in the array is the function to call that will update the value, and will take care of Reacts re-rendering for you. In the above example the onClick
of each list item is using it to setBirthdayPerson(name)
where name
is the name for that particular item in the names array.
Bringing it all together.
So now you have children
and then parents are providing props
to it. Each component can now also have their own state
for managing things, but now we want to tie these things together. Well there isn't much else to cover, just that state
and the functions to update it can be fed into props
... and this is really where the basic building blocks open into a lot of possibilities.
The Spec
We want to make an address book, names on the left that we can selected, and on the right we see more information for the selected name. We will have a data source for our address book which is just an array of objects like this:
{
_id: "5f90374ad2e52f3fbe46d149",
name: {
first: "Bentley",
last: "Rosales"
},
company: "ACUSAGE",
phone: "+1 (961) 423-2258",
address: "930 Eckford Street, Elfrida, Vermont, 1570",
photoUrl:
"https://avatars.dicebear.com/api/avataaars/5f90374ad2e52f3fbe46d149.svg"
}
We want the list to show just a first name, but on selected we want to see their address, phone number, company, and of course their picture!
Component Structure
So like everything we have a single point of entry, this will be our <App>
. Our <App>
will then have two child components <List>
- which shows our selectable list of people, and <View>
- which shows the currently selected person.
This is one of the simplest components so it makes sense to build this one first. All it needs is the right structure to render what the information we want, and a single prop
selectedPerson
.
const View = (props) => {
const { selectedPerson } = props;
return (
<div className="view">
{selectedPerson ? (
<Fragment>
<div className="view-heading">
<img src={selectedPerson.photoUrl} />
<h2>
{selectedPerson.name.first} {selectedPerson.name.last}
</h2>
</div>
<p>
<b>{selectedPerson.company}</b>
</p>
<p>{selectedPerson.address}</p>
</Fragment>
) : (
<p>No one selected</p>
)}
</div>
);
};
This is working just using props
and a single selectedPerson
prop is expected. If this isn't set we show <p>No one selected</p>
otherwise we show the data of the person.
Next up is the list component, this has to take a few different sources of information from props
. First it needs the people
which is the array of names to display. Second, it needs to know if there is a selectedPerson
so that it can show that that person is selected in the list. Finally it needs to know how to update or setSelectedPerson
so when a name is clicked it can set the selected person to whoever was clicked. All of this will be provided as props:
const List = (props) => {
const { people, selectedPerson, setSelectedPerson } = props;
return (
<div className="list">
{people.map((person) => (
<div
onClick={() => setSelectedPerson(person)}
className={
person === selectedPerson ? "list-item selected" : "list-item"
}
key={`person_${person._id}`}
>
{person.name.first}
</div>
))}
</div>
);
};
So you can see we have a list of people
which we then map and turn into <div>
elements with the persons first name rendered. We also check if the person
that we are iterating over is equal to selectedPerson
and if so we set a different CSS className
. The <div>
also gets given an onClick
function which will invoke the setSelectedPerson
prop with the respective person.
So now we have to tie the two things together, and really the only place to do this is in the parent <App>
component. This can feed the people
, selectedPerson
and setSelectedPerson
properties to the <List>
and can provide the <View>
with the selectedPerson
property.
import peopleData from "./data";
export default function App() {
const [selectedPerson, setSelectedPerson] = useState();
return (
<div className="address_book">
<List
selectedPerson={selectedPerson}
setSelectedPerson={setSelectedPerson}
people={peopleData}
/>
<View selectedPerson={selectedPerson} />
</div>
);
}
The only things we need here are to import the peopleData
from our file that has the array of people for the address book, and to create a state variable that holds the selectedPerson
. We don't provide an initial value for selectedPerson
in the useState()
call - because we have ensured that the <View>
can deal with this being empty.
You can find all of this in a neat sandbox below. Enjoy!
Posted on October 21, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024
November 27, 2024