Up till now in all of the apps that we've created where we've used state, I've actually tried to keep
the component tree very simple. And in fact all that we have is just a single App component where everything
is being rendered.
Now, we know from previous lessons that this is not how a React app will end up looking like. We would
probably want to separate out this app into separate components.
So we'd probably have a component for the heading area,
we'd probably have one for this div with the input and the button, we'd probably then have each of these
li's being a separate component being rendered.
So let's see how this would pan out.
Go ahead and fork the starting sandbox and then go into your components folder and create a new component
called ToDoItem.jsx
.
Now inside this todoItem, I want you to take out the li and create a separate component.
The behaviour that you're looking for afterwards is the ability to add new items to the list like so.
Pause the video and I'll wait for you.
All right.
So first we're going to have to import React from "react". Next,
we're going to create our function which is going to be called ToDoItem just like the name of the
file
and here we're going to return an li which is going to contain some text from the To-Do list.
Finally, we're going to export this as the default and the name is, ToDoItem.
Now we can go into our App.jsx and import our ToDoItem from "./ToDoItem".
Alright, so now we can use this component instead of this li when we want to render it. So we can add
a self-closing tag which is called ToDoItem, and then in order to display a different ToDoItem each
time we map through our items array, we're going to pass this todoItem which is the text,
this should display as a property. So we'll call it text maybe, and set it equal todoItem.
So now we can receive this text inside our ToDoItem as one of the props and instead of having this
hardcoded text, I'm going to open up a set of curly braces and happened to props.text.
So now if we refresh our app and add "Buy milk", click Add, you'll see it has exactly the same functionality
as before but this time we've now got a separate component. And that's the solution to the challenge.
This was a mini review of something we've covered already namely splitting our app into different components.
But now that we know all about state, we need to start thinking about how our state is handled across
the app and ToDoItem components. And this ToDoItem component
at the moment, is what you would call a stateless component because it's not trying to change itself
or anything else,
all it's doing is receiving some read-only properties and it just displays them inside.
Remember that props are read-only, so you can't for example, say inside here.
Let's say if we had a div and inside the div, we had an input and let's say that this input had an
onChange and when it gets called then we're going to trigger some code, and we tried to modify the props
and we say props = "new Thing".
This unfortunately does not work as I'll show you.
So I add, Buy milk,
so there's a new input that's been added as well because I'm adding a new ToDoItem.
And ideally when I start typing, it should update our props but it does not because props is read only.
So we can't modify our props, but what we can do though, is we can have state inside these components.
Here's a challenge for you.
See if you can change the code just inside this ToDoItem file so that when you add a new ToDoItem
and you click on it, it crosses out the item, and then when you click on it again it uncrosses it out.
So you've already seen what line-through does when you change the text-decoration to this particular
value. It basically just crosses out the text. And you've learnt enough about state to be able to figure
out when this li or this div gets clicked, how it should change this text styling. So you'll need to
think about inline styling, ternary operators, conditional rendering, event handling and using state to
complete this challenge. Pause the video now and give that a go.
All right.
So the first thing we need to do is test to see if we can change this li using inline styling.
So let's tap into the style property and inside the style property,
let's add some code.
So we're going to say that if we wanted the style to just be applied, then we could simply add a JavaScript
object as the code and the property was called textDecoration, which of course has to be camel case,
and then the value was called, "line-through",
and this is going to be a string.
So let's add a new item and see if that works.
So, brilliant.
That's now working.
But we need this to be rendered depending on a condition, and that condition is whether if the user has
clicked on this ToDoItem. So you can either add an onClick to the list item, or to the div.
It really doesn't actually matter.
So I'm going to add it to the div, just to keep this code on one page, and I'm going to get hold of the
onClick.
And when that happens I'm going to call a function. And the function is going to be called handleClick().
And that's what I'm going to trigger over here as well. When this div gets clicked,
I want to set a boolean maybe, to true or to false depending on whether if this textDecoration should
be applied. But let's say that I had a constant called isDone and this isDone property is either true or
false depending on whether if the user clicked on this div. And to set this isDone property, I'm going
to have a function called setIsDone and I'm of course going to need to useState to achieve this.
Now the initial value of isDone is going to be, false, because all items should start out being not done
with no line-through,
but then if the user clicks on this div, then I should be able to handle this click and call setIsDone().
And I want to set it to the opposite of what it currently is.
So I can use an arrow statement to get the prevValue and then I can return the opposite of the
prevValue.
You can of course use a bunch of if-statements, if prevValue is true, then return false, else return
true,
but actually a much easier way of doing this which you probably already know, is to simply return the
opposite of prevValue, like so.
So this way, if prevValue is true, then this is going to equal false, if it equals false then it's
going to equal again the opposite.
So now that we're handling click and we have our click added to our div, all we have to do now is to
use this isDone to conditionally render this textDecoration.
I'm going to say that this textDecoration if isDone is true ? should be the line-through,
but else the textDecoration should be set to none.
This way effectively, when this isDone is true, we're going to apply line-through text decoration,
when this is false we're going to change the textDecoration back to none, even if this was already applied.
Let's go ahead and check our app.
So add a new item, click Add. Click on it, it gets striked through. Click on it again the textDecoration
becomes none.
So this is all very well and good and we've now got state being managed inside our ToDoItem,
but notice how the state is kind of localized to this item.
It's not going anywhere else.
What if we wanted to change the state of a parent component? Let's say that when this div gets clicked
on, instead of just striking out the text,
what if we wanted to delete it from our items array? How do we reach up from our ToDoItem into its parent,
the App component, and somehow change this item's array? So first I'm going to delete that isDone component
and then I'm going to delete everything that's inside handleClick. I'm also going to delete my inline
style as well. And I'm going to delete the useState as well
so we're now back to a very simple ToDoItem.
Now we're ready to tackle the next stage.
How do we manage to add an item and when we click on it, instead of striking it through, we're changing
its styling?
I actually want to affect the entire array and this is the desired behavior.
I want to click on it, and it should delete from my array.
So if I had multiple items, say "Buy milk, Buy eggs, Buy bacon", and I decide to check off buy eggs, that should
be the only one that's removed from my array.
Let's see how we can tackle this.
The first thing to realize is that when we pass over props to our child components, we can also pass
over functions which gets called by our child component.
For example, when we create each ToDoItem, in addition to the text property, we can add another property.
So we could call it anything we want,
so say onChecked maybe. And we can set this equal to a function inside our App component.
So let's say that we wanted to create a function called deleteItem and this was the function that
we would pass to our child ToDoItem.
So we'll call it deleteItem and then inside our ToDoItem we'll now be able to trigger, instead of
handleClick which lives inside the ToDoItem,
we can now trigger props.onChecked. And this means that it'll be able to trigger deleteItem inside
our App component right here.
So let's go ahead and just log this event,
like so.
So now when I add a "Buy milk", click Add, and when I click on it you can see that in my console I'm getting
this message being sent.
So this entire function is being packaged up and sent over to the ToDoItem under the prop onChecked.
And then that prop is only triggered when this div detects a click.
And now I can address what should happen when that item wants to delete itself from the array. If I wanted
to delete an item from my items array, I'm probably going to need to call setItems.
And inside this setItems, I'll need to get hold of the prevItems and open up a set of curly braces
for my arrow function.
Now how can i delete the item that requested the deletion?
Well, one of the things I could do is when I call this onChecked, I could pass over something that identifies
this particular ToDoItem from all of the other ones that gets rendered. And I can do that by adding
an id.
So inside my to do list item,
firstly, we need to remember that when we map through arrays, and create components, we should always have
a key.
And I'm going to set this key to the index of the ToDoItem from the items array. And this map function
actually gives me a really easy way of accessing that.
Notice how in this callback function, the first item I provide is the value,
so the value of each item from the array.
So in this case it's the ToDoItem that I've already tapped into.
But if I add a comma and I add another variable name, then I can tap into the index of this item in the
items array.
So let's try doing that.
Remember from our lesson on arrays that if you only have one parameter, you can leave out the parentheses.
But if you want to have more than one parameter then you'll need the parentheses and add a comma to
separate each of those items.
So the second one I'm going to call, index.
This is going to be the index of the current todoItem that's being looped through the items array,
and I'm going to use the index as the key. Now
in an ideal world, React advises us to not use the index for the key and instead use some sort of unique
identifying string.
For example, you could use a package such as uuid which basically just allows you to generate a
uuid by installing it as a package.
So super simple. But in our case we're going to leave it as the index because we want to use this index
value, and we want to use it to pass over to our todoItem as an id.
So now that means our ToDoItem should be able to receive the id in the props.
Let's see if that is indeed the case.
So let's add a new item and then go to our components click on this item and you can see it's got the
id being passed over as 0 because it's the first item in the array.
This one has an id of 1, because it's the second item.
They both get some text as the props and they also get this method onChecked which corresponds to the
deleteItem() function.
So you might think that given that we have access to the id now inside this function, I could just
pass it back over as props.id and that way my onChecked function will be able to receive the id
right here, and then I could probably just log it.
Now I'm going to show you what actually happens instead.
If I go ahead and keep the console open and I add a new item, you'll see that immediately this console
log triggers, and it prints out the id of the item.
So what's going on here?
Now remember that way back when we talked about JavaScript functions, and passing around functions,
we said that whenever you have a set of parentheses and something inside it, then you're going to be
calling the function, rather than passing the function.
So we can't actually pass our props.id like this.
This is just going to call it immediately the moment that this ToDoItem gets rendered.
Instead what we can do in our onClick is to create a function, and this function will only get called
when the onClick gets triggered, and it's only in this moment that we actually want to tap into
props.onChecked, and then passing in the props.id. This way when the ToDoItem gets rendered,
this function is not going to be called until this div detects a click.
Let me reiterate this. Previously, when we clicked the Add button and rendered the ToDoItem component
it triggered props.onChecked immediately, but now props.onChecked will only be called the moment
the item is actually clicked.
Now if I add an item, click Add, notice that nothing gets printed until I click on this ToDoItem. Now
that we know we can get hold of the id of the ToDoItem inside this delete function,
well, now we can activate our setItems and we can use what we've learned before to change our items
array.
More specifically, we're going to use the filter function that we learned before and filter through all
of the prevItems in our items array, and get rid of the ones which match this id. So inside our
setItems,
we're going to return an array, and that array is going to be constructed from our prevItems array,
but it's going to be filtered so that we get rid of the item that we don't want,
so the one that has this id that was passed over. Remember that the filter function again expects
a function itself.
Let's go ahead and add an arrow function and inside this filter function, we have access to two properties.
The first one is the actual element that's being looped through in the array
and the second one is the index of the current element.
So we can tap into each item as we're looping through prevItems and we can also tap into the index
of that item.
Now we're going to go through our array of prevItems, look through each item, and get the index of
each item, and we're going to return an output, a final array, that is going to return all the items where
the index is not equal to the id being passed over.
So let's go ahead and hit Save and check this out inside our new window.
So I've got my React Dev Tool open here as well, and I'm going to add a new item.
So this went into the array with the index of 0, it's the first item. And the id got passed over to
the ToDoItem component as a prop.
And in addition, there's another prop, onChecked, which passed over the deleteItem() function that we were
editing here. And I've set up the code such that when I click on this ToDoItem, it will trigger deleteItem,
and it will pass over the id as the input. Then we get to receive that id here which, remember
equates to the index of the item that we're trying to delete,
and then we call setItems to update our list of items and we filter it, so that we only end up with
the ones where the index doesn't match this id,
so everything other than the one that's being clicked.
So let's add a couple more.
And if I go ahead and click on, Buy eggs, then the id is going to be 1, that's going to be passed to
deleteItem.
It's going to loop through my array of three items and filter so that we only end up with items where
the index does not equal 1.
So that's this one and this one.
So that means if I click here, you can see eggs disappears
but we end up with still the remainder of our array that's been filtered using this rule.
This really is getting into a lot of functional programming that you'll see a lot of
in React, where we're passing one function into another function, inside another function, inside another
function, inside another function.
It does take some time to be able to wrap your head around this, and I strongly encourage you to have
a go at playing around with this code, trying to filter on some sort of different condition, maybe changing
the items so that instead of being an array, it's an array of objects.
Just have a mess around with it, until you get comfortable with it.
And it always takes a lot of time to mess around with these things especially something as complex as
this to feel comfortable. And the idea is that I'm not expecting you to remember how to do this. Just
so that you understand what's going on, so you can look it up the next time.