Life brought us to the moment where we have to think about how to make our Notesy app functional. In this article we will make our Notesy app interactive such that it can show list of notes, add new notes and delete existing notes. On the way of doing so we will explore states, props, events and hooks.
If you are brand new to React, things will look little messy, because you have to adapt to the mindset of SPA development and mainly component based approach. If you are familiar with basics of JavaScript and dealing with UI using JavaScript, certain things will look like same but different as well. Same-same but different but still same!
Let us first get this straight with states, props, events and hooks; what are they and why you should care:
Some React jargons thrown on your face just like that. Don't worry, we will explain and understand each of them with code and not by words.
Now, we will resume from the point where we left in our previous article, React Journey: Part 1 - Getting Started. For this article, we will head in this direction:
NOTE: We will showcase only the snippets of code in this article to keep it short and crips. If you want to view the entire code base feel free to clone the repository and view the entire file, the links are given at the bottom of the article.
You have to sit quietly and decide what state your app wants. Here, we are talking about the main feature state on which your app's life will be dependent upon. In our Notesy app case, this is the array of notes that our app holds in such a manner that it can be populated as list of cards on the screen, and when user does add or remove this list of notes will be updated. So, we got one state after this deep though, namely "notes" state.
As this state will be used across our child components it's better to create it at root component nothing but our App.jsx:
// App.jsx
import { useState } from 'react';
...
function App() {
const [notes, setNotes] = useState([]);
...
}
Here, we created a state using a hook called "useState"; by calling this function will return 2 things. One is the state variable to read the updated value present in the state and second is the setter method to update the state whenever required. Basically, a getter and setter. Most importantly the states are immutable (they can't be updated; instead recreated). We are going with an empty array as initial value for the "notes" state.
Let us add some initial state data to the notes state, will get this data from our data.js file and supply it to the useState hook:
// App.jsx
import { notes as notesData } from './../data';
...
function App() {
const [notes, setNotes] = useState(notesData);
...
}
Now, let us pass this state to our child components, those will be needing this state data, mainly List and Empty components. This maneuver of passing state from parent to child components are simply termed as props.
// App.jsx
<List notes={notes} />
<Empty notes={notes} />
Here, we are beautifully passing the state called notes to List and Empty components. From "notes={notes}" this assignment, the left-hand side is the name of the props which we have chosen as notes as suggested by our destiny and the right-hand side is the actual state variable which is out of nowhere same as the props.
Now, by doing this dance step your child components List and Empty will receive the "notes" state. But how to take them out in these respective components. Don't think to call your ex now, just open your child components and update as per below code:
// List.jsx
const List = ({ notes }) => {
return (...);
};
// Empty.jsx
const Empty = ({ notes }) => {
return (...);
};
Just like that, if you know a little bit of the destructuring then you are pretty chill about what we are doing with the curly braces within the function's parameter tray; otherwise, checkout about destructuring in JS from here: The Art of Destructuring in JavaScript.
Let us be little format here and provide the prop types to these props:
// List.jsx
import PropTypes from 'prop-types';
const List = ({ notes }) => {
return (...);
};
export default List;
List.propTypes = {
notes: PropTypes.array,
};
// Empty.jsx
import PropTypes from 'prop-types';
const Empty = ({ notes }) => {
return (
<>
{notes && notes.length === 0 && (...)}
</>
);
};
export default Empty;
Empty.propTypes = {
notes: PropTypes.array,
};
Doing this much will give initial list of notes on your screen.
Here comes the actions sequences. We will do below tasks using events to bind the behavioral aspect of our Notesy app. These behaviors are listed below:
Will perform these for our "add" operation and then for remove.
Open your App.jsx file, create below add function and supply it as a prop to the Form child component:
// App.jsx
const add = newNotes => {
setNotes([newNotes, ...notes]);
};
<Form add={add} />
Here, in this function we are accepting a new note object and appending with our existing notes values and finally setting the notes state. As mentioned, the states are immutable, so we are passing a entirely new array with appended new note at the beginning.
Just like previous components we can accept add function as a prop in Form.jsx file:
// Form.jsx
const Form = ({ add }) => {
return (...);
};
export default Form;
Form.propTypes = {
add: PropTypes.func,
};
Let us now add the submit handler to our add button and call this add prop function which will further update the App.jsx notes state:
// Form.jsx
const Form = ({ add }) => {
const handleSubmit = event => {
event.preventDefault();
add({ id: 1, note: 'Foo Note' })
};
return (
<>
<form onSubmit={handleSubmit}>...</form>
</>
);
};
Nothing fancy, we are mainly preventing the default HTML form submission (to avoid post back) and then calling our prop add function with hard coded new object. Once you run this code you will see new note cards getting added to your list of notes on the UI. Test this and come back to proceed if you are coding along.
This is fine, but let us deal with the values entered by the user in the text field. In Reat world if you want to interact with HTML, you will be needing state. So, do not hesitate to create a state within your Form.jsx:
import { useState } from 'react';
const Form = ({ add }) => {
const [note, setNote] = useState('');
const handleNoteChange = event => {
setNote(event.target.value);
};
return (
<>
...
<input
value={note}
onChange={handleNoteChange} ... />
</>
);
};
Here, we have created a function named "handleNoteChange", this will simply update the state of note with the new value taken out from the input field using "event.target.value" (recall old HTML coding days). Also, notice that we are assigning the note state to the value attribute of input field, this is important to control our text field with the latest value of note state.
Now, let us bind this note state value to our new note object in the "handleSubmit" function. Also, we will create a unique id this time instead of the hard coded "1". This we will create using the NPM package called uuid.
// Form.jsx
import { v4 as uuid } from 'uuid';
const Form = ({ add }) => {
const handleSubmit = event => {
event.preventDefault();
add({ id: uuid(), note });
};
return (...);
};
Here, we have imported the uuid package and taken out the function named "v4()" and provided an alias called uuid (just for ease of use). This will always give us the unique id.
Life if simple when you don't care much about errors (Hehe - just a pun, don't take it seriously). We need to take care of errors and validate our field if in case user gone berserk and start hitting the keyboard. For now we are just doing the bare minimum from POV of validation and error handling. We will just check if user has entered anything or not in the field. For error handling will wrap our handle submit code within a try-catch block and show an HTML alert if something went wrong.
// Form.jsx
const Form = ({ add }) => {
const handleSubmit = event => {
try {
event.preventDefault();
if (note === '') {
window.alert('Please fill the form');
return;
}
add({ id: uuid(), note });
} catch (error) {
window.alert(`Error Occurred: ${error.message}`);
} finally {
setNote('');
}
};
return (...);
};
Here, we have wrapped our code within try-catch block as decided and added an if condition for checking empty values.
Now, we will repeat the entire step for remove operation. The remove button is present in the Item.jsx component, let us repeat all the steps:
First creating link between App.jsx, List.jsx, Item.jsx:
// App.jsx
const remove = id => {
setNotes(notes.filter(i => i.id !== id));
};
<List notes={notes} remove={remove} />
// List.jsx
const List = ({ notes, remove }) => {
return (
<>
...
<Item note={note} remove={remove} key={note.id} />
</>
);
};
// Item.jsx
const Item = ({ note, remove }) => {
return (...);
};
You can see how the event is also passed as a prop to the most desirable component which is Item.jsx. Finally, your Item.jsx will look like this:
// Item.jsx
import PropTypes from 'prop-types';
const Item = ({ note, remove }) => {
const handleRemove = () => {
try {
remove(note.id);
} catch (error) {
window.alert(`Error Occurred: ${error.message}`);
}
};
return (
<>
...
<button onClick={handleRemove}></button>
...
</>
);
};
export default Item;
Item.propTypes = {
note: PropTypes.object,
remove: PropTypes.func,
};
Looks beautiful, isn't it? We have added the error handling mechanism same as previous one.
Run and check the whole functionality, create, read and delete. If getting errors, try to run the provided repository code and then compare. Leave comments if still face issues.
Let us recall here what it is the App.jsx doing here after completing till this point. The App.jsx serving as a parent for all these child components Form, List and Empty. You will find such situation in React more often, where you parent is there to hold a common state for the children. In such situation restrict your parent to only do so, and not much more than that. Because if you add additional features to such arrangements then your parent will be deviated from what it was intended at the first place. So, avoid adding any service calls or other logics to such parent to make them hold the concept of SRP.
We are breaking this principal in one area, where we are taking the task of List.jsx component in our own hand. The List.jsx component should be self-sufficient to bring its own data from API call or some static file. What the App.jsx will do is to get that value from List.jsx and update Empty.jsx when ever got fresh data. In this case let us move the logic where we are assigning the initial state dirctly to the App.jsx notes state. Just like other component the App.jsx will be notified once the data is obtained using a event passing mechanism which we have already been completed using add and remove method.
Let us create one more method called "getAll", this method will update the state of the notes and nothing else. Also, make the initial value as empty array for notes state.
// App.jsx
const [notes, setNotes] = useState([]);
const getAll = notes => {
setNotes(notes);
};
<List notes={notes} remove={remove} getAll={getAll} />
Now, why we are talking about this under "Create Hooks" header. Because we will be needing one hook to achieve further functionality named "useEffect". If you not already but noticed that we are using hook in our code; useState is a hook itself.
The useEffect hook that let you handle the side effects. Anything that goes beyond simply rendering UI then will be handled using useEffect. The useEffect comes with dependency array that tracts the side effects to listen. If you keep the array empty the code within the useEffect function will be executed once when the component initial renders. Thus, we get a component loads/rendered kind of function where we can make our initialization or call an API or bring data.
We will use the useEffect hook inside List.jsx component to make it stand on its own for getting data. Once List.jsx receives the data it will call the getAll event of App.jsx.
// List.jsx
import { useEffect } from 'react';
import { notes as notesData } from './../../data';
const List = ({ notes, remove, getAll }) => {
useEffect(() => {
getAll(notesData);
}, []);
return (...);
};
With this simple change we can call the getAll of App.jsx and loads the data from List.jsx and not from App.jsx itself.
Note that this is one kind of approach and not a must, so if you are using some other way around and if it works for you then no problem (life is short), do what works for you the best. The approach what we discussed is an ideal one which in general you will come across (thanks to Context API/Redux this approach also not voted - problem of prop drilling).
Now, relax take a deep breath and go through the entire stuffs that we have been talked about in this article and check how our code is behaving. Leave console.log where you want to understand what's happening, and how the data flow is happening.
If you are facing errors feel free to leave comments and also refer the GitHub repository code, for this article refer "react-2-crud" project.
December 31, 2020
October 19, 2020
March 02, 2022