Exercise: Throw It Away Obscurely

Exercise 9 Average

Prerequisites for the exercise

  1. React State

Objective

Rewrite the RemovableItemList component to reimplement the item removal logic in a different way.

Description

In the previous Throw It Away exercise, we implemented a RemovableItemList component that rendered a list of items, each of which could be removed using a 'Remove' button.

Upon removal, the corresponding item from an internally maintained items array was filtered out and the new array used to update the state.

Now, in this exercise, the scenario is different.

In this exercise, you have to reimplement RemovableItemList in a way that when an item is removed, the items array (i.e. the state value) isn't reduced in length.

At each point in time, the length of the items array (not the items prop provided to the component) must be the same as the length of the items prop provided to the component.

Besides, the items state array must be an array of objects, each having two properties:

  • text — specifies the text of the item.
  • removed — a Boolean, specifies whether the item has been removed or not.

Using this array of objects, you have to render the list of <li> elements.

Another thing to keep in mind is that you must NOT mutate anything inside the removeItem() function (that we defined in the exercise) — every value should be made from scratch inside the function.

On the outside, this program obviously works in the exact same way as the program we implemented in the previous exercise. However, internally, it clearly differs in how it implements the removal logic.

As before, following is a dummy usage of this component:

function App() {
   return (
      <RemovableItemList items={[
         'Python',
         'JavaScript',
         'PHP',
         'Dart'
      ]}/>
   );
}

Live Example

View Solution

New file

Inside the directory you created for this course on React, create a new folder called Exercise-9-Throw-It-Away-Obscurely and put the .js solution files for this exercise within it.

Solution

Here's the implementation of RemovableItemList from the last exercise, Throw It Away:

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

First things first, let's change the value that is used to initialize the state value items. We need an array of objects, likewise we use map() to create an object for each item.

This step is pretty basic and accomplished as follows:

function RemovableItemList({ items: initialItems }) {
const [items, setItems] = useState(initialItems.map(item => ({
text: item,
removed: false
}))); 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> ); }

Great!

Now the next thing to do is to modify the code rendering the list of <li> elements. In particular, if the current item (in line 14) has not been removed (i.e. has removed set to false), only then could we render it; otherwise, we render nothing for the item.

Also, the rendered value item needs to be replaced with item.text (since item is now an object, not a string, and it's invalid to render an object in React).

This is accomplished as follows:

function RemovableItemList({ items: initialItems }) {
   const [items, setItems] = useState(initialItems.map(item => ({
      text: item,
      removed: false
   })));

   function removeItem(index) {
      setItems(items.filter((_, i) => i !== index));
   }

   return (
      <ul>
         {items.map((item, i) => (
!item.removed && (
<li key={i}>{item.text} <button onClick={() => removeItem(i)}>Remove</button></li>
) ))} </ul> ); }

So far, so good.

The last thing left now is to fix the removeItem() function.

Recall that we're not allowed to reduce the length of the items array; we can only modify the individual elements in it. Likewise, we can't keep using the filter() method inside removeItem().

But even when modifying the individual items, this has to be done in a 'pure' fashion as instructed in the exercise's description above — we are not allowed to mutate even the individual objects of items.

Fortunately, using the spread operator (...), this is really easy.

Here's the new implementation of removeItem():

...
   function removeItem(index) {
      setItems(items.map((item, i) => {
         if (i === index) {
            return { ...item, removed: true };
         }
         return { ...item };
      }));
   }
...

The setItems() function is provided a new array by mapping over items; in this way, the length of the array always remains the same in every single render.

The mapping function has a very simple logic: if the index of the current item is the same as index (the index of the item to be removed), we return a new object having the same properties as item but with removed: true; otherwise, the item object is merely copied over and returned.

It's time to test this new implementation now:

function App() {
   return (
      <RemovableItemList items={[
         'Python',
         'JavaScript',
         'PHP',
         'Dart'
      ]}/>
   );
}

Live Example

Perfect!

With this, we complete this exercise.