DEV Community

NextjsVietnam
NextjsVietnam

Posted on

ReactJS Course - Lesson 03 - Props and States

Note: All source code for this course is publicly available on github at ReactJS Tutorial 2023

Lesson 03

  1. Learn the theory of props, state, hooks and illustration
  2. Learn the theory of how to organize data structures in applications and illustrate
  3. Summary

In the previous article, you have learned the structure of a ReactJS application which is a combination of components.

Learn about props, state, hooks through examples

  1. Design button "New Link" and LinkFormComponent
  • Install bootstrap
  • Handle Event when pressing "New Link" button will open Modal without input form for new link
  • In the input form for the new link, it is required to enter the link, the title part if the user leaves it blank, use the entered link as the title.
  • When clicking Close/Esc will close the modal. When reopening the Modal for New Link, the form data must be empty.
  • When you click Save Changes in the main screen, it will display the JSON format of the added links.

Using bootstrap

npm install bootstrap sass --save
Enter fullscreen mode Exit fullscreen mode

index.html

<!DOCTYPE html>
<html lang="en">
   <head>
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>%VITE_APP_NAME% - %MODE%</title>
   </head>
   <body>
     <div id="root" class="container"></div>
     <script type="module" src="/src/main.jsx"></script>
   </body>
</html>
Enter fullscreen mode Exit fullscreen mode

main.jsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
// Bootstrap Bundle JS
import "bootstrap/dist/js/bootstrap.bundle.min";
// reset css
import "./index.scss";

ReactDOM.createRoot(document.getElementById("root")).render(
   <React.StrictMode>
     <App />
   </React.StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

index.scss

$primary: #176d16;
$danger: #ff4136;

@import "node_modules/bootstrap/scss/bootstrap";
Enter fullscreen mode Exit fullscreen mode

Event Handler in ReactJS

<a
   href="https://nextjsvietnam.com"
   className="btn btn-primary"
   onClick={(e) => {
     e.preventDefault();
     alert(`You're going to redirect to ${e.target.href}`);
   }}
>
   Event Handler
</a>
Enter fullscreen mode Exit fullscreen mode

Props

In ReactJS, components often use props to communicate with each other. Parent components will usually pass data to child components via props. In addition, props also includes HTML attributes, values in Javascript such as: objects, arrays, including functions.

import { useState } from "react";
import enviroment from "./shared/environment";

const Counter = ({ number, children, ...props }) => {
   return (
     <>
       <a {...props}>
         Counter Name:{children}
         <br />
         Value: {number}
       </a>
     </>
   );
};

const Lesson003 = () => {
   const values = [1, 2, 3, 4, 5, 6, 7, 8, 9];
   return (
     <>
       {values.map((v, index) => (
         <Counter key={index} number={v} className="btn btn-primary">
           Counter {index + 1}
         </Counter>
       ))}
     </>
   );
};

export default Lesson003;
Enter fullscreen mode Exit fullscreen mode

There are 3 points to keep in mind here when using props:

  1. All attributes passed as attributes of a tag/component are included in props. However, use the spread operator to select certain properties, this makes it easy for the editor to assist you in the coding process.
  2. In the above example you will see the appearance of chidren, this keyword means that the content inside the 2 opening and closing tags of the component will be assigned to the prop with the key children.
  3. Props are readonly, never try to change the value of props (immutable). Always use the underlying state when a variable type is needed to change the value each time the user interacts.

One note, when rendering a list in ReactJS, we will see the following error appear on the console.

image

The reason is that in the design of ReactJS, in order to optimize rendering and DOM updates, when rendering a list of items, assigning a unique key to each element in the array, will help ReactJS optimize rendering when an item is added/removed/updated.

State

It is essentially the component's memory, representing the current state of the component and will change when the user interacts with the component

Consider the following example

import enviroment from "./shared/environment";

const Lesson003 = () => {
   let counter = 1;
   console.log("Before render");
   return (
     <>
       <h2>State and Event</h2>
       <h3>Counter: {counter}</h3>
       <button
         type="button"
         className="btn btn-primary"
         onClick={(e) => {
           e.preventDefault();
           console.log(`Increase counter from ${counter} to ${counter + 1}`);
           counter += 1;
           console.log("After counter increased!", counter);
         }}
       >
         Increase counter
       </button>
     </>
   );
};

export default Lesson003;
Enter fullscreen mode Exit fullscreen mode

As we have seen, the event handler is very similar to handling an event on the DOM, just a little different in syntax, is that instead of calling the function immediately, in JSX you need to assign an event to a function. This function can be an anonymous function as in the example, or a previously declared function.

<button onclick="myFunction()">Click me</button>
Enter fullscreen mode Exit fullscreen mode

Looking at the above code, you will think, when the counter variable changes, on the screen now, you will see its new value. However, keep an eye on the console.log and the results of each click of the Increase counter button.

image

The expected result is that after each counter mutate, console.log will record an extra line of "Before render". And the counter on the interface will also be updated.

The answer is, React Component has no reason to re-render, it just re-renders if and only when state changes. Below is the syntax to declare and use state in ReactJS.

import { useState } from "react";
import enviroment from "./shared/environment";

const Lesson003 = () => {
   const [counter, setCounter] = useState(0);
   console.log("Before render, counter:", counter);
   return (
     <>
       <h2>State and Event</h2>
       <h3>Counter: {counter}</h3>
       <button
         type="button"
         className="btn btn-primary"
         onClick={(e) => {
           e.preventDefault();
           console.log(`Increase counter from ${counter} to ${counter + 1}`);
           setCounter(counter + 1);
           console.log("After counter increased!", counter);
         }}
       >
         Increase counter
       </button>
     </>
   );
};

export default Lesson003;
Enter fullscreen mode Exit fullscreen mode

image
After rewriting the original code, using the syntax in React to declare and update the state. We notice three things:

  1. The state cannot be updated directly, only the provided setState function can be used.
  2. When the setState function is called, the value of state does not change immediately.
  3. After the Component is re-rendered, we will see the new value of the state.
  4. If the state is an object, it should be noted that the new value of the state must form a new object, then React will recognize the changed state and re-render (because the object in javascript is always referenced to a fixed address at initialization and during the value change process, this reference address remains unchanged).

Suppose, we call the state update function consecutively, what will happen:

import { useState } from "react";
import enviroment from "./shared/environment";

const Lesson003 = () => {
   const [counter, setCounter] = useState(0);
   console.log("Before render, counter:", counter);
   return (
     <>
       <h2>State and Event</h2>
       <h3>Counter: {counter}</h3>
       <button
         type="button"
         className="btn btn-primary"
         onClick={(e) => {
           e.preventDefault();
           console.log(`Increase counter from ${counter} to ${counter + 1}`);
           setCounter(counter + 1);
           setCounter(counter + 2);
           setCounter(counter + 3);
           console.log("After counter increased!", counter);
         }}
       >
         Increase counter
       </button>
     </>
   );
};

export default Lesson003;
Enter fullscreen mode Exit fullscreen mode

Maybe 6 or so? As a result, only setCounter(counter+3) is executed.

image

Explanation: In an event, when the state update happens, ReactJS will collect these updates and execute it once, at this time the state counter will be a fixed value in each update event, so even though the previous state update events of +1,+2 are performed, but at step number +3, the counter value is still taken from the original value, so the result will be as we have seen.
Let's test updating 2 separate states in the same event and check how many times ReactJS will render.

import { useState } from "react";
import enviroment from "./shared/environment";

const Lesson003 = () => {
   const [counter, setCounter] = useState(0);
   const [counter2, setCounter2] = useState(1);
   console.log("Before render, counter:", counter, "counter2:", counter2);
   return (
     <>
       <h2>State and Event</h2>
       <h3>Counter: {counter}</h3>
       <h3>Counter2: {counter2}</h3>
       <button
         type="button"
         className="btn btn-primary"
         onClick={(e) => {
           e.preventDefault();
           console.log(`Increase counter from ${counter} to ${counter + 2}`);
           console.log(`Increase counter from ${counter2} to ${counter2 + 3}`);
           setCounter(counter + 2);
           setCounter2(counter2 + 3);
         }}
       >
         Increase counter
       </button>
     </>
   );
};

export default Lesson003;
Enter fullscreen mode Exit fullscreen mode

image

If instead of using state value directly in component, and update state using callback function like below

import { useState } from "react";
import enviroment from "./shared/environment";

const Lesson003 = () => {
   const [counter, setCounter] = useState(0);
   const [counter2, setCounter2] = useState(1);
   console.log("Before render, counter:", counter, "counter2:", counter2);
   return (
     <>
       <h2>State and Event</h2>
       <h3>Counter: {counter}</h3>
       <h3>Counter2: {counter2}</h3>
       <button
         type="button"
         className="btn btn-primary"
         onClick={(e) => {
           e.preventDefault();
           console.log(`Increase counter from ${counter} to ${counter + 2}`);
           console.log(`Increase counter from ${counter2} to ${counter2 + 3}`);
           setCounter((counter) => counter + 2);
           setCounter((counter) => counter + 4);
           setCounter2((counter2) => counter2 + 3);
         }}
       >
         Increase counter
       </button>
     </>
   );
};

export default Lesson003;
Enter fullscreen mode Exit fullscreen mode

The results are exactly as described.

image

Further testing with setTimeOut,

import { useState } from "react";
import enviroment from "./shared/environment";

const Lesson003 = () => {
   const [counter, setCounter] = useState(0);
   const [counter2, setCounter2] = useState(1);
   console.log("Before render, counter:", counter, "counter2:", counter2);
   return (
     <>
       <h2>State and Event</h2>
       <h3>Counter: {counter}</h3>
       <h3>Counter2: {counter2}</h3>
       <button
         type="button"
         className="btn btn-primary"
         onClick={(e) => {
           e.preventDefault();
           console.log(`Increase counter from ${counter} to ${counter + 2}`);
           console.log(`Increase counter from ${counter2} to ${counter2 + 3}`);
           setCounter((counter) => counter + 2);
           setCounter((counter) => counter + 4);
           setTimeout(() => {
             setCounter((counter) => counter + 6);
           }, 0);
           setCounter2((counter2) => counter2 + 3);
         }}
       >
         Increase counter
       </button>
     </>
   );
};

export default Lesson003;
Enter fullscreen mode Exit fullscreen mode

As a result, we have 2 renders, because the update of state counter + 6 takes place in another process.

image

Same test with object for statestate

import { useState } from "react";
import enviroment from "./shared/environment";

const Lesson003 = () => {
   const [stateObject, setStateObject] = useState({
     a: {
       b: {
         c: 1,
       },
     },
   });
   console.log("Before render, stateObject.a.b.c:", stateObject.a.b.c);
   return (
     <>
       <h2>State and Event</h2>
       <h3>StateObject: {JSON.stringify(stateObject)}</h3>
       <button
         type="button"
         className="btn btn-primary"
         onClick={(e) => {
           e.preventDefault();
           setStateObject((obj) => {
             console.log(obj.a.b.c, "obj === stateObject", obj === stateObject);
             obj.a.b.c += 1;
             return obj;
           });
         }}
       >
         Increase counter
       </button>
     </>
   );
};

export default Lesson003;
Enter fullscreen mode Exit fullscreen mode

As a result, even though we see the value of c has changed, in fact the UI still does not re-render.

image

Therefore, mutating the object directly and the resulting change still refer to the same object, will make the React Component not aware that it will have to re-render, because it performs the comparison between the two objects.
Therefore, it is imperative that you do not change this object directly, but rather create an object consisting of the values of the old object mixed with the new changed value.

import { useState } from "react";
import enviroment from "./shared/environment";

const Lesson003 = () => {
   const [stateObject, setStateObject] = useState({
     a: {
       b: {
         c: 1,
       },
     },
   });
   console.log("Before render, stateObject.a.b.c:", stateObject.a.b.c);
   return (
     <>
       <h2>State and Event</h2>
       <h3>StateObject: {JSON.stringify(stateObject)}</h3>
       <button
         type="button"
         className="btn btn-primary"
         onClick={(e) => {
           e.preventDefault();
           setStateObject((obj) => {
             console.log(obj.a.b.c, "obj === stateObject", obj === stateObject);
             return {
               ...obj,
               a: {
                 b: {
                   c: obj.a.b.c + 1,
                 },
               },
             };
           });
         }}
       >
         Increase counter
       </button>
     </>
   );
};

export default Lesson003;
Enter fullscreen mode Exit fullscreen mode

image

Now both on the UI, and on the console, the value of c is updated accordingly.

How to get the value of form input

import { useState } from "react";

const Lesson003 = () => {
   const [input, setInput] = useState("");
   const [items, setItems] = useState([]);

   return (
     <>
       <h2>State Array</h2>
       <div className="mb-3">
         <label htmlFor="exampleFormControlInput1" className="form-label">
           Item name
         </label>
         <input
           value={input}
           onChange={(e) => {
             setInput(e.target.value);
           }}
           type="email"
           className="form-control"
           id="exampleFormControlInput1"
           placeholder="name"
         />
       </div>
       <button
         type="button"
         className="btn btn-primary"
         onClick={(e) => {
           e.preventDefault();
           setItems((items) => {
             return [...items, input];
           });
         }}
       >
         Add new item
       </button>
       <ul>
         {items.map((item, index) => (
           <li key={index}>{item}</li>
         ))}
       </ul>
     </>
   );
};

export default Lesson003;
Enter fullscreen mode Exit fullscreen mode

image

So we can see that by using state, and the onChange event, you will be able to synchronize the user input and the state of this input. Thanks to the mechanism of ReactJS, in the picture you only see the re-rendered part is the content of the input, not the whole, which makes the application so smooth.

Let's try with state as array

<button
   type="button"
   className="btn btn-primary"
   onClick={(e) => {
     e.preventDefault();
     setItems((items) => {
       return [...items, input];
     });
   }}
>
   Add new item
</button>
Enter fullscreen mode Exit fullscreen mode

Through the above examples, we can conclude the following:

image

However, the above way of writing makes the code look quite complicated and difficult to maintain. You can use the immutable library to still ensure the immutable state of the state, but when you write the code, it looks like it's mutating.

npm install use-immer --save
Enter fullscreen mode Exit fullscreen mode
import { useState } from "react";
import { useImmer } from "use-immer";

const Lesson003 = () => {
   const [input, setInput] = useState("");
   const [items, setItems] = useImmer([]);

   return (
     <>
       <h2>State Array</h2>
       <div className="mb-3">
         <label htmlFor="exampleFormControlInput1" className="form-label">
           Item name
         </label>
         <input
           value={input}
           onChange={(e) => {
             setInput(e.target.value);
           }}
           type="email"
           className="form-control"
           id="exampleFormControlInput1"
           placeholder="name"
         />
       </div>
       <button
         type="button"
         className="btn btn-primary"
         onClick={(e) => {
           e.preventDefault();
           setItems((items) => {
             items.push(input);
           });
         }}
       >
         Add new item
       </button>
       <ul>
         {items.map((item, index) => (
           <li key={index}>{item}</li>
         ))}
       </ul>
     </>
   );
};

export default Lesson003;
Enter fullscreen mode Exit fullscreen mode

This new code looks much cleaner, doesn't it?

The process of rendering a ReactJS Component will proceed as follows:

Trigger -> Render -> Commit

Trigger:

  • When the application is initialized at this step, ReactJS will embed the application in the DOM node root, and call the render method of the components.
  • Re-render: every time the component updates its state, it will re-render itself, which leads to its children components re-rendering as well.

Render:

As mentioned above, when the application initializes, React will call the root component to perform the rendering.

  • For the next time, React will actually call the function components whose state update triggers this rendering.

Commit:

  • When the application initializes, after the root component renders successfully, React will now append all the generated DOM nodes to the DOM tree.
  • For subsequent iterations, React will do the comparison and only update the DOM tree if there is actually a change.

So you have an overview of state, props in ReactJS and how ReactJS re-renders components when state changes.

Practice building a link management application

Application requirements

In real application, we need to solve more conundrums. Within the scope of this lesson, we seek to answer the following questions.

Suppose, you have to build a small application, to manage a list of links.
Each link can be: Website Link, Image(png/jpg/jpeg), Youtube Link.

The application will include a single screen, adding and editing tasks will open a modal that allows the user to enter/change information, after saving, the information in the list will be updated.

Main features of the application include:

  • Add, Edit, Delete Link
  • Display links in 3 formats: website (link form with entered title, if title is blank, use link), image (image format with entered title, if blank, use link), youtube link (iframe with embed link)
  • Store application information on the user's machine, so that when the application is closed and reopened, the saved information still exists.

Interface requirements are as follows

Home screen

Homescreen

Modal add, edit

Modal add, edit

Feel free to read full course at ReactJS Course 2023

From Khóa Học ReactJS

Top comments (0)