useState and Hooks
by Tomas Trescakยท React

Root Folder
Not Attempted
NameProgress
Introduction
Not Read
๐Ÿฐ Player
Not Read
Setup
Not Attempted
NameProgress
Software Installation
Not Read
Project Setup
Not Read
Running and Testing
Not Read
React and ReactDOM
Not Read
๐Ÿ’ก Assignment 1: Welcome Message
Not Attempted
Submissions
Not Read
JSX and Components
Props
๐Ÿ‘พ Exercise: Props
Not Attempted
CSS Styles
useState and Hooks
๐Ÿ‘พ Exercise: useState
Not Attempted
Conditional Rendering
Lists
๐Ÿ‘พ Exercise: Lists
Not Attempted
Forms and Events
๐Ÿ‘พ Exercise: Forms
Not Attempted
๐Ÿ’ก Assignment 2: Front End
Not Attempted
Advanced React
Not Attempted
NameProgress
Pure Components - memo
Not Read
Lifecycle - useEffect
Not Read
Expensive? - useMemo
Not Read
DOM Interactions - useRef
Not Read
forwardRef
Not Read
useImperativeHandle
Not Read
๐Ÿ‘พ useImperativeHandle
Not Attempted
Context
Not Read
useCallback
Not Read
useId
Not Read
useReducer
Not Read
Infrastructure
Not Attempted
NameProgress
Database
Not Read
NextAuth and Github Authentication
Not Read
Prisma and ORM
Not Read
Project Setup
Not Read
Project Authentication
Not Read
APIs
Not Attempted
NameProgress
APIs
Not Read
APIs - Slides
Not Attempted
Rest APIs
Not Read
Rest APIs - Express.js
Not Read
ReastAPIs - Next.js
Not Read
Securing APIs
Not Read
Securing APIs - NextAuth
Not Read
tRPC
Not Attempted
NameProgress
tRPC
Not Read
tRPC - Routers
Not Read
tRPC - Server Rendering
Not Read
tRPC - Client Rendering
Not Read
Persisting Data
Not Read
Assignment 3: APIs
Not Read
0 / 1070 XP

Lecture Transcript
If you prefer text to video, check out the transcript of the presentation above

Let's finally have som fun and start building truly reactive React components!

In React, "reactivity" refers to the way in which React applications respond to changes in state or props by automatically updating the user interface (UI). This concept is central to Reactโ€™s design and helps make it a powerful library for building dynamic web applications.

Thus, instead of telling the webpage how to change the website after the state has changed, we describe how the page should look based on the current state and let React take care of the changes.

All we need to do is tell react when to initiate the state change. In our case, we change the state when clicking the button. After every click, the state changes and a new count is rendered. Let's explore more!

While React also supports the definition of Components using classes ...

... we will focus on a more modern representation of React components using functions, also known as functional components. In React, functional components use special functions called hooks to control the life cycle and behaviour of react components.

You can easily spot the react hook as it will have the โ€œuseโ€ prefix.

For example, we will use useState hook to control the state and reactivity of the component ...

useEffect to control the side effects and clean up after the component

useMemo to remember values of expensive computations and many others. This section will focus on the useState hook to control the reactivity of functional React components.

Let's revisit the previous example and discuss React state in detail! State in React determines the behaviour and rendering of components. When state data changes, React re-renders the component to reflect the updates. Try clicking the plus button to see how the component automatically renders a new value.

In functional components, we will use the useState hook from the React library to control the component state.

The useState hooks return an array of two elements.

The first array element is the value from the state ...

... and the second one is a function we use to change the value.

We use array destructuring to assign values to variables.

To manage the state, we often define custom handlers, such as this inc function, which increases the state value by one.

We can then show the current value in a position where we desire, with its own custom look and styles.

We also assign our custom state change handler to the button's onClick function. Therefore, every time we click the button, it will call the inc function, which will change the state.

The last thing we need to mention is that the state change function can also accept a producer function that takes one parameter, a current value of the state variable and returns a new state. This is often useful when dealing with asynchronous state changes, ensuring the state never becomes stale.

Let's take a look at a more complex example, where we store arrays in the React state. Similar to our previous example, we initialise the state and state change function, this time storing the array value.

In the state change handler function, we simply push a new string to the state array and store the same array in the state.

Then, we wire our state change handler to the button click ...

And show all the array content, separated by a comma. But when you click on the button, nothing happens! When dealing with React state, the most important thing to realise is that you must provide a different state value to re-render the component. Otherwise, no change will happen. This is simple with atomic values such as string or number but becomes more complicated with objects or arrays, which use object references. React makes only object reference comparisons for performance reasons.

Therefore, to change the state, it is not sufficient to update the array. You have to create a new array to create a new object reference. In this case, we use the convenient spread operator to change the array.

When you click on the plus button now, all is well as react recognises that a state has been updated.

If you do not like the spread operator, you can use the concat function to create a new array combining two provided array. So, please remember that functions such as push, pop, shift or unshift that modify existing array in-place will not work for React state changes and a new object reference to a modified array must be provided.

With objects, situation is the same as with arrays. Changing object properties does not change the object reference and you have to update the state with a new object value. Consider this example.

First, we initialise the state that is an object value, holding name and age of a person.

Then, we define our state handler where we increase the age of the person and store it back to the state. Can you already spot the error?

Then, we show the name and age inside a div.

Last, we wire the button click to the state change handler. But when we click the button, the person's age does not update! Let's fix it.

Let's fix it! The goal is to produce a new state, in our case a new object. We will use the convenient spread operator to create the new state. Similarly you could use the assign function to create a new object but this will do!

When we run our example now, all works as expected!

Sometimes, you may want to use the same component state repeatedly in your app across many components.

For example, you want to track and modify this user data across more components in your app.

For such purpose, we create a new usePersonData to track user data and we also add some extra functionality to simplify their use.

For example, we will initialise user data with some default values and allow to specify only a subset of required values.

We also add a new method that will allow to specify only a subset of user data that we want to update, simplifying data updates.

Now, we are ready to use this hook. Note that we use object destructuring and not array as in regular hooks, as we return an object from our custom hook, not an array.

We can now use our fancy new method to use partial data to update the state, reducing the need for spread operators.

We can use the data as with a regular hook. Pretty cool, huh? Online, you may find collections of custom hooks simplifying common operations, and reducing the boilerplate in your applications. Let's take a look now at some limits of hooks and the rules we need to follow when using them,

When using hooks you need to remember two important rules. Rule #1: Hooks can only be called from within the functional components.

For example, the following code would fail and rendering this component would lead to runtime warning telling you that hooks can only be called inside the body of a function.

To fix it, you must move hooks inside the component and the code will work.

If you paid attention, you might argue that custom hooks are not components, yet we use hooks inside them!

Well, even though the custom hook is NOT a component, we are calling it from within the component. That is why we are not violating the number one rule of hooks. React knows, that the hook was called from within a component.

Rule #2 dictates that you must render the same number of hooks on each component render. Ergo, you cannot conditionally call hooks.

In this example, the first click of the button works just fine, but clicking the button a second time crashes the application.

The reason for this is that when the value of the count reaches one, we initialise a new state variable that breaks the rule of hooks.

The way to fix this is to extract the conditional code into a custom component and track its state separately.

In React, state updates are batched to optimize performance and minimize unnecessary re-renders. Batching means that multiple state updates within the same event are combined into a single re-render. LEt's take a look at the example.

We print the message that the component is rendering.

The console.log prints the stale state (values before the update).

setCount1 and setCount2 are called sequentially. React batches both updates and processes them together in one re-render.

Then, we log the count values and observe their value. Do you think the value changed?

Let's take a look at a console, what we see there once we click the button. The rendering happens only once! The updates happened in batch!

The Before Updates log will show the old values.

The After Updates log will still show stale values because state updates are asynchronous.

Description
All the extra information about this section

React = Reactivity

In React, "reactivity" refers to the way in which React applications respond to changes in state or props by automatically updating the user interface (UI). This concept is central to Reactโ€™s design and helps make it a powerful library for building dynamic web applications. Thus, instead of telling the webpage how to change the website after the state has changed, we describe how the page should look and let React take care of the changes. 

The primary tool for reactivity in React is the state. Managing state in React involves understanding how to efficiently handle operations like adding, removing, and updating items within arrays. This section explores these fundamental concepts using the React state management system.

๐ŸชHooks

While React also supports the definition of Components using classes, we will focus on a more modern representation of React components using functions, also known as functional components. In React, functional components use special functions called hooks to control the life cycle and behaviour of react components. You can easily spot the react hook as it will have the โ€œuseโ€ prefix. For example, we will use:

  • useState hook to control the state and reactivity of the component

  • useEffect to control the side effects and clean up after the component

  • useMemo to remember values of expensive computations

  • โ€ฆ and others

In this section, we focus on the most use hook of them all โ€œuseStateโ€

Introduction to State in React

State in React is a set of data that determines the behaviour and rendering of components. When state data changes, React re-renders the component to reflect the updates. The state is typically initialised in a component's constructor (for class components) or using the useState hook in functional components. 

Below is an example of using state in a functional component:

function Counter() {
  const [value, setValue] = useState(0);
  
  function inc() {
    setValue(value + 1);
  }
  
  return (
    <div>
      Counter: {value}
      <button onClick={}>+</button>
    </div>
  )
}

The useState hooks return an array of with two elements. First array element is the value from the state and the second one is a function we use to change the value.  We use array destructuring to assign values into variable. 

Furthermore, in our example, the button increases the count value. Without telling React how to update the value on the screen the counter value is increased. Magic! ๐Ÿช„

Producer Functions

The setValue in the example above either accepts a new value or a producer function. Producer functions assure that the value used to compute the new state is never stale. This can often happen when using custom hooks, useMemo hook or asynchronous operations.  For example:

 function Counter() {
  const [value, setValue] = useState(0);
  
  function inc() {
    // producer function
    setValue((value) => value + 1);
  }
  
  return (
    <div>
      Counter: {value}
      <button onClick={}>+</button>
    </div>
  )
}

We will show some examples of stale state once we start exploring the useMemo hook.

State with Complex Values

Here, the things get tricky. The most important thing to realise is that you have to provide a different value in order to re-render the component, otherwise no change will happen. This is simple with atomic values such as string or number, but becomes more complicated with objects or arrays, which use object references.  React does only shallow (object reference) comparison for performance reasons.

Arrays

To change the state it is not sufficient to update the array. You have to create a new array to create a new object reference. In the following example, pressing the โ€œ+โ€ button does nothing.

function NameList() {
    const [names, setNames] = useState(["Tomas"]);
    
    function insertName() {
        names.push("Valeria");
        setNames(names)
    }
    
    return (
        <div>
            {names.join(", ")}
            <Button onClick={insertName}>+</Button>
        </div>
    )
}

We have modified only the internal properties and structure of the array, retaining the same object reference. Similarly doing any of the operations on the array will not produce any state change:

  • push

  • pop

  • shift

  • unshift

  • splice

  • reverse

Instead, we need to use methods that modify the array and return a nbew object reference such as:

Let's fix the example above in a couple of ways.

function NameList() {
    const [names, setNames] = useState(["Tomas"]);
    
    function insertName() {
        // fix 1
        setNames(names.concat(["Valeria"]))
        
        // or, fix 2 
        setNames([...names, "Valeria" ])
    }
    
    return (
        <div>
            {names.join(", ")}
            <Button onClick={insertName}>+</Button>
        </div>
    )
}

Objects

With objects, situation is the same as with arrays. Changing object properties does not change the object reference and you have to update the state with a new object value. Consider the following failing example:

function NameList() {
    const [person, setPerson] = useState({ name: "Tomas", age: 44 });
    
    function increaseAge() {
        person.age += 1;
        setPerson(person);
    }
    
    return (
        <div>
            Name: { person.name  }<br />
            Age: { person.age }
            <Button onClick={increaseAge}>+</Button>
        </div>
    )
}

Before you look at the solution below, try to fix the example yourself.! Did you do it? If not, check out our solution, we fix it in two different ways, using a spread operator and using Object.assign function.

function NameList() {
    const [person, setPerson] = useState({ name: "Tomas", age: 44 });
    
    function increaseAge() {
        // fix 1
        setPerson(Object.assign(person, { age: person.age + 1});
        
        // or fix 2
        setPerson({
            ...person,
            age: person.age + 1
        })
    }
    
    return (
        <div>
            Name: { person.name  }<br />
            Age: { person.age }
            <Button onClick={increaseAge}>+</Button>
        </div>
    )
}


While both ways do the same, it is better to use the spread operator as it is easier to read and works seamlessly for complex components. In the next section, let's work on an exercise using useState.

๐Ÿช Custom Hooks

Often, you keep using the same component state repeatedly in your app across many component. Or, you mimic the same component behaviour across your app. For such purpose, you can create a custom hook. For example, imagine that you track person data in many components, and you want to only use partial data to initialise the person information. For such functionality, we add a usePersonData hook:

function usePersonData(partialPerson: Partial<Person>) {
    const [person, setPerson] = useState<Person>({
        name: "John",
        lastName: "Doe",
        age: 0,
        ...partialPerson
    })
    return {
        person,
        setPerson,
        setPartialPerson(partialPerson: Partial<Person>) {
            setPerson({ ...person, ...partialPerson })
        }
    }
}

function NameList() {
    const { person, setPartialPerson } = usePersonData({ name: "Tomas", age: 44 });
    
    function increaseAge() {
        setPartialPerson({
            // ...person // no need for this!
            age: person.age + 1
        })
    }
    
    return (
        <div>
            Name: { person.name  }<br />
            Age: { person.age }
            <Button onClick={increaseAge}>+</Button>
        </div>
    )
}

As you can see, cusom hook can help create helper functions simplifying the overall complexity of your application code.

๐Ÿช Rules of Hooks

When using hooks you need to remember two important rules:

Rule #1: Hooks can only be called from within the functional components. 

For example, the following would fail:

const [count, setCount] = useState(0)

function App() {
    return <div>{count}</div>
}

Rendering this component would lead to runtime error. To fix it, you must move hooks inside the component:

function App() {
    const [count, setCount] = useState(0)
    return <div>{count}</div>
}

But, you may argue that custom hooks are NOT functions, yet we use react hooks within! For example:

function useCounter() {
    const [count, setCount] = useState(0)
    return { count, setCount }
} 

function App() {
    const { count } = useCounter();
    return <div>{count}</div>
}

Well, even though the custom hook is NOT a component, we are calling it from within the component. That is why we are not violating the number one rule of hooks. React knows, that the hook was called from within a component.

Rule #2: You must render the same number of hook on each component render. ergo, you cannot conditionally call hooks. 

For example, this example would fail:

function App() {
  const [count, setCount] = useState(0)
  if (count > 0) {
      const [double, setDouble] = useState(count * 2)
      return <div onClick={() => setDouble(double * 2)}>{double}</div>
  }
  return <div onClick={() => setCount(count + 1)}>{count}</div>
}

In the above example we tracked state in the count variable, until it reached 1. Then we decided to start doubling the value. Unfortunately, this breaks the rule of hooks as we can not render different number of hooks and you would see a following error in your console:

To fix this you need to track both hooks on the component level:

function App() {
  const [count, setCount] = useState(0)
  const [double, setDouble] = useState(count * 2)
       
  return (
      count == 0 
      ? <div onClick={() => setCount(count + 1)}>{count}</div>
      : <div onClick={() => setDouble(double * 2)}>{double}</div>
  )
}

๐Ÿญ Batching of State Updates with useState

In React, state updates are batched to optimize performance and minimize unnecessary re-renders. Batching means that multiple state updates within the same event are combined into a single re-render.


How Batching Works

  1. What is Batching?
    • If you call setState multiple times in a single event handler, React groups these updates and performs only one render.
  2. When Does Batching Happen?
    • React automatically batches state updates during synchronous events (e.g., onClick).
    • In React 18 and later, asynchronous updates like setTimeout or API calls are also batched.

Example: Batching in Action

Hereโ€™s an example to demonstrate how React batches state updates:

import React, { useState } from "react";

const BatchingDemo = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const handleClick = () => {
    console.log("Before Updates:", count1, count2);

    // Multiple state updates in one event
    setCount1((prev) => prev + 1);
    setCount2((prev) => prev + 1);

    console.log("After Updates (Still stale):", count1, count2);
  };

  return (
    <div>
      <h1>React State Batching Example</h1>
      <p>Count 1: {count1}</p>
      <p>Count 2: {count2}</p>
      <button onClick={handleClick}>Increment Both</button>
    </div>
  );
};

export default BatchingDemo;

Explanation:

  1. Before State Update:
    The console.log prints the stale state (values before the update).
  2. State Updates:
    • setCount1 and setCount2 are called sequentially.
    • React batches both updates and processes them together in one re-render.
  3. After Event:
    The re-render occurs once, and both states are updated simultaneously.

What You See in the Console

When you click the button:

  1. The Before Updates log will show the old values.
  2. The After Updates log will still show stale values because state updates are asynchronous.
  3. After the render completes, both count1 and count2 are updated.

 

Maggie

Discuss with Maggie
Use the power of generative AI to interact with course content

Discussion

0 comments
Loading editor ...
Remember to be polite and report any undesirable behaviour

Category

Empty

Labels

Discussion has no labels

1 participant

user avatar

Priority

Notifications

You're not receiving notifications from this thread.
Course Outline