React Props & State: Exercise 3 – Events

This is the third exercise in the series. You may want to start with the first one and the second one.

React is a JavaScript library for building dynamic websites. It works by reacting to changes in the data, and making changes in what the user sees based on those changes. The data it reacts to are called props and state.

Props – Input data to the component

State – Internal state data of the component

Set Up

If you haven’t already, you’ll need to get the exercise files. If you already got them when you did the first exercise, you don’t need to get them again.

To get them, clone this repository to your computer:
https://github.com/natafaye/props-and-state-practice

You can clone it using git with this command:

git clone https://github.com/natafaye/props-and-state-practice

Open the props-and-state-practice folder in Visual Studio Code.

Open a terminal in this folder (Ctrl + ` or Terminal > New Terminal)

Install the dependencies that are in the package.json file, with this command:

npm install

Exercise 3: Class Components

Expand the src folder, then the exercise-3-class folder.

In this folder there are three class components, the App component, the Contact component, and the ContactList component. You can mix functional and class components, but for this exercise we’ll use all class components to practice with them.

There is also a data.js file that holds some static data that we will use. In a real app, we might pull our data from a server, but for this exercise we’ll just pull it from the data.js file.

Set Up

We need to make sure our index.js is using the App component in this folder. Open index.js in the src folder, and make sure that the import for Exercise 3: Class Component is uncommented:

// Exercise 3: Class Components
import App from './exercise-3-class/App';

All the other App imports should be commented out.

The Goal

In Exercise 1 we set up our Contact component to show data about a contact.

In Exercise 2 we added internal state data (a list of contacts) to our app and set up our ContactList component to show a list of contacts.

Now we want our app to be dynamic, and allow the user to change the state of the app. There are two buttons: one in the Contact component that deletes one contact, another in the ContactList component that deletes all the contacts. These buttons should change the internal state data which will trigger a re-render of the app.

So, when it’s all completed, our app will work like this:

Tips

The Contact component is nearly identical to the one in Exercise 1, with an addition. You can copy your code from Contact.js in the exercise-1-class folder, into Contact.js in the exercise-3-class folder. Then add in the new Delete button:

<li className="list-group-item text-end">
    <button className="btn btn-danger">Delete</button>
</li>

The ContactList component is nearly identical to the one in Exercise 2, with an addition. You can copy your code from ContactList.js in the exercise-2-class folder, into ContactList.js in the exercise-3-class folder. Then add in the new Delete All button.

<div className="row mb-3">
    <div className="col text-end">
        <button className="btn btn-danger btn-lg">Delete All</button>
    </div>
</div>

Because we now want to return two divs from our render function, and React only allows us to return one parent element, we need to use React.Fragment to wrap both divs, like this:

<React.Fragment>
    <div className="row">
        CONTACTS HERE
    </div>
    <div className="row mb-3">
        <div className="col text-end">
            <button className="btn btn-danger btn-lg">Delete All</button>
        </div>
    </div>
</React.Fragment>

When we update our state, we should not modify the state directly. That means that if you are storing an array in state, you should not add or remove items directly from that array. You can use an array method that makes a copy (some examples are .filter() and .map() and .concat() and .slice() ), or make a copy yourself and then make the change to the copy. You then set the state to the changed copy.

In React, the component that stores the data in its state is the only one that can change the data. State is internal to a component and no other component can change it. Usually the component that stores the data in its state will have functions that modify the state in all the needed ways (like deleting something, creating something, or updating something). If any of its child components need to be able to be able to modify the data, it will pass the needed function down to that child component through props. Then the child component can call that function when it needs to.

Hints

What general steps should I follow here?

Here is a general idea of the steps you’ll probably want to follow:

  1. Write a function that modifies the state. Usually this is a CRUD modification, for example, deleting a task or creating a comment. Make sure that function is inside the component that has the data in its state (since state is internal to a component and should only be modified from inside that component)
  2. Pass that function down through props to the component(s) that need it. This is typically the component(s) that handle the user event(s) that need to modify the data, like the component that has the button for creating a comment.
  3. Set up the event handler(s) in the needed component(s) and have them call the passed down function. The event handler(s) could be for when the user clicks a button or checks a checkbox. They can’t change the data themselves, because the data is in the state of a different component. So they call the passed down function and ask it to modify the data.

More specific please. What steps should I follow for this app?

Here’s what those steps translate to for this app:

  1. Write a function that modifies the state. We need a function that deletes a Contact and that needs to be in the App component, because that’s the component that has the list of contacts in its state.
  2. Pass that function down through props to the component(s) that need it. The ContactList and Contact component need that function for their Delete All and Delete buttons to work, so we’ll need to pass it down to them.
  3. Set up the event handler(s) in the needed component(s) and have them call the passed down function. Our Delete button needs an event handler that just calls the passed down function with whatever parameter it needs, to know which Contact to delete. Our Delete All button needs an event handler that loops through all the contacts and calls the passed down function with each one of them.

How do I access the list of contacts I stored in the state of the App component?

In a class component, you can access any state variable using:

this.state.nameOfStateVariable

Where do I put the function that deletes a contact?

In a class component, functions for modifying the state should be methods on the class. It’s common to name them something like handleDeleteContact or onDeleteContact. This isn’t a firm rule, it just can help make your code more readable.

If you make the method with a normal function, you’ll need to bind this so it will have the correct value when the function is called. You bind it in the constructor, and it looks something like this:

this.methodName = this.methodName.bind(this);

If you make the method with an arrow function, you do not need to bind this.

This method needs to be in the component that has the list of contacts in its state, because you cannot modify the state of one component from another component.

How do I change something in the state?

In a class component, you can set the state using the setState method. Something like this:

this.setState({ nameOfStateVariable: newValueOfStateVariable })

If the new value is dependent on the current value (like incrementing a share counter, or changing a piece of an array) you need to use a callback function to make the new value of the state variable and set the state. That will ensure you’re using the most up to date current value when you make the new value.

It might look a bit like this:

this.setState( state => ({ nameOfStateVariable: codeThatMakesNewValue }) )

(We put parenthesis around the object we’re returning from our arrow function, because if we just put curly brackets Javascript will get confused and think those curly brackets are for the function body.)

So for a share counter, maybe it looks like this:

this.setState( state => ({ shareCount: state.shareCount + 1 }) )

Or for something more complex, like this:

this.setState(state => {
    // code that makes the new value of the state variable based on the state parameter
    return {
        nameOfStateVariable: newValueOfStateVariable
    }
});

How can I delete a contact from the array without modifying the state directly?

Ooh, this is the tricky part.

There are many ways to do this. My favorite way is with the .filter() array method.

This array method returns a copy of the array with everything that returns true from the callback function, and without anything that returns false. So all we need is a callback function that will return true for everything in the array EXCEPT the data item we want to delete.

One of the best ways to identify the data item we want to delete is by the id (sometimes the id property is called id, sometimes _id, sometimes ID – make sure you’re using the correct property name for the data you’re working with). That way it will work properly even if other data on the data item changes, or if we’re working with a copy of the data item, because the id is unique and never changes.

So a function like this:

dataItem => dataItem.id !== dataItemToDeleteId

…will return true for everything in the array EXCEPT the data item we want to delete. Perfect!

Pairing that with filter and simplifying it a little, we get something like this:

const newArrayOfDataItems = this.state.arrayOfDataItems(d => d.id !== dataItemId)

(I used the letter d for my parameter name to be short for dataItem. If you are working with todos you might want to call your parameter t. If you’re working with messages you might want to call your parameter m. But you can call it anything you want)

You could then set the state to be the new array of data items with the correct data item removed.

BUT WAIT.

We have one problem. When we call the setState function it may not run immediately. React does a lot of work in the background to run efficiently, so it doesn’t always run calls to setState immediately. Which means, we could have a very tricky bug if we call setState with our new array of data items minus one, but in the time between when we created that array and when setState was called there have been other changes to the array of data items.

Let’s walk through what that bug would look like.

What if we had an array like this in our state:

[
    {
        id: 0,
        name: "Maria"
    },
    {
        id: 1,
        name: "Simone"
    }
]

And say we want to delete Maria. So we filter the array for only items that do not have the id of 0. That gives us this array:

[
    {
        id: 1,
        name: "Simone"
    }
]

We call setState with that filtered array and think everything is good.

BUT IT’S NOT GOOD.

Maybe React waits a little to actually run that call to setState and change the state. In that time, maybe someone adds a new person to the array (or maybe they added a person earlier, and the call to setState just ran). If React runs that call to setState first, our state will get set to this:

[
    {
        id: 0,
        name: "Maria"
    },
    {
        id: 1,
        name: "Simone"
    },
    {
        id: 2,
        name: "Chance"
    }
]

And then our other call to setState runs, and it sets the state to be the filtered array, and our new state is:

[
    {
        id: 1,
        name: "Simone"
    }
]

Chance got un-added! You can imagine in a big app with lots of changes happening to the data very quickly, this could become a major problem. And it wouldn’t happen all the time, so it can be a very hard bug to catch. This is what’s called a race condition in programming. Meaning, the app will run differently depending on what code wins the “race” and gets run first. We don’t want that! We want our code to run the same whether Chance gets added first or Maria gets deleted first.

So we use a callback function. Rather than making our new state array and then passing it to setState, we’ll pass setState a callback function that knows how to make the new state array based on the current state.

For deleting, that callback function could look something like this:

state => ({ arrayOfDataItems: state.arrayOfDataItems.filter(d => d.id !== dataItemId) })

Pairing that with the setState function, we get our final, working solution:

this.setState(state => ({ arrayOfDataItems: state.arrayOfDataItems.filter(d => d.id !== dataItemId) })

Okay, I have a function that deletes a contact. How do I get that to my ContactList and Contact components?

Pass it as a prop! You’ll need to first pass it to the ContactList component (the prop for this sort of thing is commonly named something like onContactDelete to mimic native Javascript events like onClick and onKeyPress). Then ContactList can use the function wherever it needs to and it can pass it as a prop to all its Contact components so they can use the function as well.

How many props & state variables should I use here?

In total I used one state variable in the App component, two props in the ContactList component, and two props in the Contact component.

How do I tell React to run some code when I click a button?

You can use the onClick attribute and set it to the function you want React to call when you click the button.

It might look like this:

<button onClick={ functionToCall } >Click Me</button>

Or this:

<button onClick={ this.methodToCall } >Click Me</button>

(If it’s a normal function method – not an arrow function – you’ll need to bind this to the method in the constructor)

Or this:

<button onClick={ () => functionToCall(argumentToPass) }>Click Me</button>

I have access to the function that deletes a contact in the ContactList component. How do I use that to delete all the contacts?

You’ll probably want a method on your ContactList component for this. Your button can call that method when it’s clicked on.

In that method, you need to loop over all the contacts in the list of contacts and call the function that deletes a contact for each one.

Another option is to make a function that deletes all the contacts in App and pass that function to ContactList and have it just call that function.

I need more help. What might the function that deletes a contact look like?

I named my method handleDeleteContact, and it looked like this:

handleDeleteContact(contactId) {
    this.setState(state => ({ contacts: state.contacts.filter(c => c._id !== contactId) }) );
}

I need more help. What might the event handler for the Delete button look like?

I just used an arrow function right in the onClick attribute, it looked like this:

onClick={ () => this.props.onDeleteContact(this.props.contact._id) }

Since my handleDeleteContact() function expects the id of the contact to delete as a parameter, I passed in the id for the contact this Delete button is on.

I need more help. What might the event handler for the Delete All button look like?

I made a method for this one, since it took a bit more code:

handleDeleteAll = () => {
    this.props.contacts.forEach(c => this.props.onDeleteContact(c._id));
}

I used the forEach() array method to call the delete function on each contact in the list of contacts.

I connected the onClick attribute of the Delete All button to the handleDeleteAll() method. Because I used an arrow function I didn’t have to bind this to the method in the constructor.

onClick={this.handleDeleteAll}

Exercise 3: Functional Components

Expand the src folder, then the exercise-3-functional folder.

In this folder there are three functional components, the App component, the Contact component, and the ContactList component. You can mix functional and class components, but for this exercise we’ll use all functional components to practice with them.

There is also a data.js file that holds some static data that we will use. In a real app, we might pull our data from a server, but for this exercise we’ll just pull it from the data.js file.

Set Up

We need to make sure our index.js is using the App component in this folder. Open index.js in the src folder, and make sure that the import for Exercise 3: Functional Component is uncommented:

// Exercise 3: Functional Components
import App from './exercise-3-functional/App';

All the other App imports should be commented out.

The Goal

In Exercise 1 we set up our Contact component to show data about a contact.

In Exercise 2 we added internal state data (a list of contacts) to our app and set up our ContactList component to show a list of contacts.

Now we want our app to be dynamic, and allow the user to change the state of the app. There are two buttons: one in the Contact component that deletes one contact, another in the ContactList component that deletes all the contacts. These buttons should change the internal state data which will trigger a re-render of the app.

So, when it’s all completed, our app will work like this:

Tips

The Contact component is nearly identical to the one in Exercise 1, with an addition. You can copy your code from Contact.js in the exercise-1-functional folder, into Contact.js in the exercise-3-functional folder. Then add in the new Delete button:

<li className="list-group-item text-end">
    <button className="btn btn-danger">Delete</button>
</li>

The ContactList component is nearly identical to the one in Exercise 2, with an addition. You can copy your code from ContactList.js in the exercise-2-functional folder, into ContactList.js in the exercise-3-functional folder. Then add in the new Delete All button.

<div className="row mb-3">
    <div className="col text-end">
        <button className="btn btn-danger btn-lg">Delete All</button>
    </div>
</div>

Because we now want to return two divs from our render function, and React only allows us to return one parent element, we need to use React.Fragment to wrap both divs, like this:

<React.Fragment>
    <div className="row">
        CONTACTS HERE
    </div>
    <div className="row mb-3">
        <div className="col text-end">
            <button className="btn btn-danger btn-lg">Delete All</button>
        </div>
    </div>
</React.Fragment>

When we update our state, we should not modify the state directly. That means that if you are storing an array in state, you should not add or remove items directly from that array. You can use an array method that makes a copy (some examples are .filter() and .map() and .concat() and .slice() ), or make a copy yourself and then make the change to the copy. You then set the state to the changed copy.

In React, the component that stores the data in its state is the only one that can change the data. State is internal to a component and no other component can change it. Usually the component that stores the data in its state will have functions that modify the state in all the needed ways (like deleting something, creating something, or updating something). If any of its child components need to be able to be able to modify the data, it will pass the needed function down to that child component through props. Then the child component can call that function when it needs to.

Hints

What general steps should I follow here?

Here is a general idea of the steps you’ll probably want to follow:

  1. Write a function that modifies the state. Usually this is a CRUD modification, for example, deleting a task or creating a comment. Make sure that function is inside the component that has the data in its state (since state is internal to a component and should only be modified from inside that component)
  2. Pass that function down through props to the component(s) that need it. This is typically the component(s) that handle the user event(s) that need to modify the data, like the component that has the button for creating a comment.
  3. Set up the event handler(s) in the needed component(s) and have them call the passed down function. The event handler(s) could be for when the user clicks a button or checks a checkbox. They can’t change the data themselves, because the data is in the state of a different component. So they call the passed down function and ask it to modify the data.

More specific please. What steps should I follow for this app?

Here’s what those steps translate to for this app:

  1. Write a function that modifies the state. We need a function that deletes a Contact and that needs to be in the App component, because that’s the component that has the list of contacts in its state.
  2. Pass that function down through props to the component(s) that need it. The ContactList and Contact component need that function for their Delete All and Delete buttons to work, so we’ll need to pass it down to them.
  3. Set up the event handler(s) in the needed component(s) and have them call the passed down function. Our Delete button needs an event handler that just calls the passed down function with whatever parameter it needs, to know which Contact to delete. Our Delete All button needs an event handler that loops through all the contacts and calls the passed down function with each one of them.

How do I access the list of contacts I stored in the state of the App component?

In a functional component, you can access any state variable using:

nameOfStateVariable

Where do I put the function that deletes a contact?

In a functional component, functions for modifying the state should just be created and saved in variables inside the component function. It’s common to name them something like handleDeleteContact or onDeleteContact. This isn’t a firm rule, it just can help make your code more readable.

If you used an arrow function, it might look something like this:

export default function ComponentName(propsCouldBeHere) {
    // some code here
    
    const handleSomeEvent = (any, parameters, would, go, here) => {
        // code for handling the event
    }

    // more code and the return
}

This function needs to be in the component that has the list of contacts in its state, because you cannot modify the state of one component from another component.

How do I change something in the state?

In a functional component, you can set the state using the state setting function you got from the useState() hook.

So if you set up your state like this:

const [nameOfStateVariable, setNameOfStateVariable] = useState(initialValueOfStateVariable);

You can set the state like this:

setNameOfStateVariable(newValueOfStateVariable);

If the new value is dependent on the current value (like incrementing a share counter, or changing a piece of an array) you need to use a callback function to make the new value of the state variable and set the state. That will ensure you’re using the most up to date current value when you make the new value.

It might look a bit like this:

setNameOfStateVariable( currentValue => codeThatMakesNewValue );

So for a share counter, maybe it looks like this:

setShareCount( currentShareCount => currentShareCount + 1 )

Or for something more complex, like this:

setNameOfStateVariable(currentValue => {
    // code that makes the new value of the state variable based on the currentValue parameter
    return newValueOfStateVariable;
});

How can I delete a contact from the array without modifying the state directly?

Ooh, this is the tricky part.

There are many ways to do this. My favorite way is with the .filter() array method.

This array method returns a copy of the array with everything that returns true from the callback function, and without anything that returns false. So all we need is a callback function that will return true for everything in the array EXCEPT the data item we want to delete.

One of the best ways to identify the data item we want to delete is by the id (sometimes the id property is called id, sometimes _id, sometimes ID – make sure you’re using the correct property name for the data you’re working with). That way it will work properly even if other data on the data item changes, or if we’re working with a copy of the data item, because the id is unique and never changes.

So a function like this:

dataItem => dataItem.id !== dataItemToDeleteId

…will return true for everything in the array EXCEPT the data item we want to delete. Perfect!

Pairing that with filter and simplifying it a little, we get something like this:

const newArrayOfDataItems = arrayOfDataItems(d => d.id !== dataItemId)

(I used the letter d for my parameter name to be short for dataItem. If you are working with todos you might want to call your parameter t. If you’re working with messages you might want to call your parameter m. But you can call it anything you want)

You could then set the state to be the new array of data items with the correct data item removed.

BUT WAIT.

We have one problem. When we call the state setting function it may not run immediately. React does a lot of work in the background to run efficiently, so it doesn’t always run calls to a state setting function immediately. Which means, we could have a very tricky bug if we call the state setting function with our new array of data items minus one, but in the time between when we created that array and when that state setting function was called there have been other changes to the array of data items.

Let’s walk through what that bug would look like.

What if we had an array like this in our state:

[
    {
        id: 0,
        name: "Maria"
    },
    {
        id: 1,
        name: "Simone"
    }
]

And say we want to delete Maria. So we filter the array for only items that do not have the id of 0. That gives us this array:

[
    {
        id: 1,
        name: "Simone"
    }
]

We call the state setting function with that filtered array and think everything is good.

BUT IT’S NOT GOOD.

Maybe React waits a little to actually run that call to the state setting function and change the state. In that time, maybe someone adds a new person to the array (or maybe they added a person earlier, and the call to the state setting function just ran). If React runs that call to the state setting function first, our state will get set to this:

[
    {
        id: 0,
        name: "Maria"
    },
    {
        id: 1,
        name: "Simone"
    },
    {
        id: 2,
        name: "Chance"
    }
]

And then our other call to the state setting function runs, and it sets the state to be the filtered array, and our new state is:

[
    {
        id: 1,
        name: "Simone"
    }
]

Chance got un-added! You can imagine in a big app with lots of changes happening to the data very quickly, this could become a major problem. And it wouldn’t happen all the time, so it can be a very hard bug to catch. This is what’s called a race condition in programming. Meaning, the app will run differently depending on what code wins the “race” and gets run first. We don’t want that! We want our code to run the same whether Chance gets added first or Maria gets deleted first.

So we use a callback function. Rather than making our new state array and then passing it to the state setting function, we’ll pass the state setting function a callback function that knows how to make the new state array based on the current state array.

For deleting, that callback function could look something like this:

currentArrayOfDataItems => currentArrayOfDataItems.filter(d => d.id !== dataItemId)

Pairing that with the state setting function, we get our final, working solution:

setArrayOfDataItems( currentArrayOfDataItems => currentArrayOfDataItems.filter(d => d.id !== dataItemId) )

Okay, I have a function that deletes a contact. How do I get that to my ContactList and Contact components?

Pass it as a prop! You’ll need to first pass it to the ContactList component (the prop for this sort of thing is commonly named something like onContactDelete to mimic native Javascript events like onClick and onKeyPress). Then ContactList can use the function wherever it needs to and it can pass it as a prop to all its Contact components so they can use the function as well.

How many props & state variables should I use here?

In total I used one state variable in the App component, two props in the ContactList component, and two props in the Contact component.

How do I tell React to run some code when I click a button?

You can use the onClick attribute and set it to the function you want React to call when you click the button.

It might look like this:

<button onClick={ functionToCall } >Click Me</button>

Or this:

<button onClick={ this.methodToCall } >Click Me</button>

(If it’s a normal function method – not an arrow function – you’ll need to bind this to the method in the constructor)

Or this:

<button onClick={ () => functionToCall(argumentToPass) }>Click Me</button>

I have access to the function that deletes a contact in the ContactList component. How do I use that to delete all the contacts?

You’ll probably want a function in your ContactList component for this. Your button can call that event handler function when it’s clicked on.

In that function, you need to loop over all the contacts in the list of contacts and call the function that deletes a contact for each one.

Another option is to make a function that deletes all the contacts in App and pass that function to ContactList and have it just call that function.

I need more help. What might the function that deletes a contact look like?

I named my function handleDeleteContact, and it looked like this:

const handleDeleteContact = (contactId) => {
    setContacts( currentContacts => currentContacts.filter(c => c._id !== contactId) );
}

I need more help. What might the event handler for the Delete button look like?

I just used an arrow function right in the onClick attribute, it looked like this:

onClick={ () => onDeleteContact(contact._id) }

Since my handleDeleteContact() function expects the id of the contact to delete as a parameter, I passed in the id for the contact this Delete button is on.

I need more help. What might the event handler for the Delete All button look like?

I made a function for this one, since it took a bit more code:

const handleDeleteAll = () => {
    contacts.forEach(c => onDeleteContact(c._id));
}

I used the forEach() array method to call the delete function on each contact in the list of contacts.

I connected the onClick attribute of the Delete All button to the handleDeleteAll() function.

onClick={handleDeleteAll}

Conclusion

Were you able to get all the exercises working? Do you understand how to use props and state a little better?

Leave a Reply

Your email address will not be published. Required fields are marked *