Objective
Create a RemovableItemList
component to render a list of removable items.
Description
Let's say we have a list of given items, wherein each item has a 'Remove' button next to it as illustrated below:
- Python
- JavaScript
- PHP
- Dart
The button, as its name suggests, serves to remove the corresponding item from the list completely.
For example, if we click the button on the first item, here's the list we'd get:
- JavaScript
- PHP
- Dart
We can imagine that the button actually reads "Throw this item away," but we won't use the text itself because it's a little bit rude. (Do you think it is?)
In this exercise, you have to create a RemovableItemList
component to render such a list.
Here are a couple of points to note regarding the implementation of RemovableItemList
:
- The list of items should be provided to the component via an
items
prop, in the form of an array. - To update the state of the component, you must only use an array method that returns a new array; NOT one which mutates the original array.
Shown below is a dummy usage of this component:
function App() {
return (
<RemovableItemList items={[
'Python',
'JavaScript',
'PHP',
'Dart'
]}/>
);
}
Here's the program you should get in the end:
New file
Inside the directory you created for this course on React, create a new folder called Exercise-8-Throw-It-Away and put the .js solution files for this exercise within it.
Solution
Before we begin, first let's spare a few minutes understanding what exactly do we need to create, and only then proceed to actually creating it.
We need to define a component called RemovableItemList
that renders a <ul>
list containing a series of <li>
elements, each of which could be removed by virtue of a button inside it.
Now since the button causes a UI change, it's apparent that it's meant to trigger a state update. But where?
Well, if we think for a second, we see that it makes perfect sense to give a state to RemovableItemList
, indicating the current list of items rendered in it. Initially, this state would be set to the given items
array but as we interact with the 'Remove' buttons, it would keep on reducing down until it becomes empty.
Let's call this state as items
.
Each button's click handler would serve to modify this items
state. And that's by removing the corresponding item from items
and then providing the new array (with the item removed) to the state update function.
And that's essentially it.
Let's get to code all this beautiful logic.
First things first, let's define the basic structure of RemovableItemList
:
import React, { useState } from 'react';
function RemovableItemList({ items: initialItems }) {
const [items, setItems] = useState(initialItems);
return (
<ul>
{items.map((item, i) => (
<li key={i}>{item} <button>Remove</button></li>
))}
</ul>
);
}
Notice the way we destructure the given props
argument, extracting the prop items
into a variable called initialItems
. We do this because we want to create a state value called items
later on in the function; it's impossible to do so if there is a similarly-named prop.
Also notice the application of key
to each <li>
element. This is necessary because React requires the key
prop when we render lists. Besides this, since there is no need of real uniqueness right now, we're good to go with the index of each list item as the key.
With this code in place, let's now set up a click handler on each button:
function RemovableItemList({ items: initialItems }) {
const [items, setItems] = useState(initialItems);
return (
<ul>
{items.map((item, i) => (
<li key={i}>{item} <button onClick={() => removeItem(i)}>Remove</button></li>
))}
</ul>
);
}
When a button is clicked, it removes the corresponding item from the items
array by virtue of a function removeItem()
.
This removeItem()
function is defined as follows:
function RemovableItemList({ items: initialItems }) {
const [items, setItems] = useState(initialItems);
function removeItem(index) {
setItems(items.filter((_, i) => i !== index));
}
return (
<ul>
{items.map((item, i) => (
<li key={i}>{item} <button onClick={() => removeItem(i)}>Remove</button></li>
))}
</ul>
);
}
As required by the exercise, we aren't allowed to use anything besides an array method (for e.g. a for
loop, a while
loop, or any other imperative code) to perform the removal of an item.
Plus, we can't even use an array method that mutates the original array. We're only allowed to use a method that returns a new array.
And for this, the perfect choice is the filter()
method.
These requirements second the architecture of React
Note that these requirements aren't impractical — they really second the functional architecture of React, whereby functions don't perform side effects and return new values after processing given values. In other words, it is desirable to:
- Use array methods only to modify arrays.
- Use only those array methods that are pure.
Obviously, in rare cases, we might want to ditch this recommended approach but almost always there is no need to, and so shouldn't we too.
When called on an array and provided with a callback function, filter()
lets in only certain elements from the array into a new, filtered array.
In our case, we want to filter the items
array, letting in only those items whose index is not equal to the index
argument provided to removeItem()
. And that's determined by the simple check i !== index
.
Now the good news is that we're done! Yes, done!
Let's test our component on the same dummy example given in the exercise's description above:
import RemovableItemList from './RemovableItemList';
function App() {
return (
<RemovableItemList items={[
'Python',
'JavaScript',
'PHP',
'Dart'
]}/>
);
}
Perfect! Just as we wanted.
And this completes this exercise.