A Brief History of React

It's unusual to think about now, but there was a time when React was mostly written through class-based components rather than the functional components everyone is familiar with in the present.

Class Components

When React was first introduced in 2013, it relied on class-based components. These allowed developers to define UI elements as JavaScript classes, providing a structured and reusable way to create complex user interfaces. Class components also offered important React features such as state and lifecycle methods, which made it possible to manage component data and handle events over time. Additionally, context was introduced as a way to pass data between components without the need for prop drilling.

Functional Components

While revolutionary at the time, class components came with a myriad of problems. They were verbose and often required a lot of boilerplate code, making the code harder to read and maintain. Then came functional components, defined as JavaScript functions, which provided a cleaner, more concise way to create UI elements.

React Hooks

React hooks were introduced in React 16.8 as a way to bring powerful features like state management, lifecycle methods, and context into functional components. Before hooks, these features were only available in class components, but with hooks, developers can now write cleaner, more concise code without sacrificing functionality.

Basic Rules

1) Hooks can only be called within React Functional Components

Hooks can only be used within React Functional Components (RFCs) or custom hooks. You cannot call hooks inside React Class Components (RCCs), as doing so will throw an error.

2) Hooks must be called at the top level of a component

Hooks must always be called at the top level of an RFC or custom hook. This means you cannot call hooks inside nested functions, loops, conditional statements, event handlers, or try-catch blocks. Keep in mind that this rule applies to calling the hook itself, not its usage — as will be explained later on.

3) Ensure a hook contains the correct dependencies if it has any

Hooks often rely on values like state or props that can change and affect the hook's behavior. When these values change, the hook must be updated to reflect the new state. This is why we pass dependencies in a dependency array, to ensure the hook re-runs when the values in the array change.

Main Hooks

1) useState()

2) useEffect()

3) useRef()

Below is a brief guide on the application of each of these hooks, along with common mistakes to avoid and tips for utilizing them effectively.

useState()

The most common and essential hook in React, useState() is used in nearly every React project. When a change is made to a variable in a functional component (e.g., a user clicks a button), and you want to rerender the component to reflect that change, you must use useState(). Simply using regular variables declared with let won’t work because changing the value of such variables doesn’t trigger a rerender. To update the component’s UI based on the new value, the variable must be treated as state. Only changes to state or props can trigger a component re-render.

Example

use-state-example.jsx

1import React, { useState } from 'react';
2
3function ToggleButton() {
4  const [isOn, setIsOn] = useState(false);
5  // Declare a state variable "isOn" and a function to update it, "setIsOn"
6
7  // Function to toggle the value of "isOn"
8  const toggleState = () => {
9    setIsOn(prevState => !prevState); // Toggle the state between true and false
10  };
11
12  return (
13    <div>
14      <p>The button is {isOn ? 'ON' : 'OFF'}</p>
15      <button onClick={toggleState}>Toggle</button>
16    </div>
17  );
18}
19
20export default ToggleButton;

Explanation

The above example shows a simple toggle switch. First, we must call useState() at the top level of the component. Doing so returns an array with two items: the state variable and the setter function for that state. We destructure this array and assign the values to a variable that will represent our state, and a setter function for updating the value. The usual naming convention is to prefix the setter function with 'set', followed by the name of the state variable. The argument inside the parentheses represents the initial value of the state.

How to Replace State

State is immutable in React, meaning it can't be directly changed. Instead, what should be done is replacing the state object with a new value, which will trigger a rerender of the component. The proper way to do this is by calling the setter function, in this case, setIsOn(), with the new value. You should never change the state directly, and you can't because it is treated as a constant.

useEffect()

Often, you need to handle asynchronous or timed events when working with UI interactions. useEffect() is a hook created for this purpose. It handles three main use cases:

1) Asynchronous functions: Such as API calls using the Fetch API or Axios library.

2) Timed events: Such as delays or intervals.

3) Events: HTML and JavaScript events.

Application

useEffect() takes a single argument: a function that runs after the component has rendered. The second argument, which is optional, is an array of dependencies. If any of the dependencies are replaced, the function will be triggered again. While the second argument is optional, it is highly recommended and commonly used to prevent the code inside useEffect() from running unnecessarily.

There are three possible values you can pass as the second argument:

1) Undefined: The code inside useEffect() runs after every render.

2) Empty Array []: The code inside useEffect() runs only after the initial render.

3) Array of Dependencies: The code inside useEffect() runs only when one or more of the dependencies change.

Example

use-effect-timer-example.jsx

1import React, { useState, useEffect } from 'react';
2
3function Timer() {
4  // State to store the counter value
5  const [count, setCount] = useState(0);
6
7  // useEffect to set up the timer when the component mounts
8  useEffect(() => {
9    // Set up the interval to increment the counter every second
10    const intervalId = setInterval(() => {
11      setCount(prevCount => prevCount + 1);
12    }, 1000); // 1000ms = 1 second
13
14    // Cleanup function to clear the interval when the component unmounts
15    return () => clearInterval(intervalId);
16  }, []); // The empty dependency array ensures this runs only once when the component mounts
17
18  return (
19    <div>
20      <h1>Timer: {count} seconds</h1>
21    </div>
22  );
23}
24
25export default Timer;
26

Explanation

The above example shows a simple timer that ticks every second. First, we call useState() at the top level of the component to set the initial count of the timer, which in this case is 0. Then, after the component renders for the first time, we run the code block inside the useEffect() function. This code does three things:

1) Declares the interval.

2) Sets count to increment by 1 every 1000 milliseconds (1 second).

3) Clears the interval by returning a cleanup function from useEffect().

It’s important to understand that useEffect() only needs to be called once. You might be confused and think that, since we want the timer to run indefinitely, we should add count to the dependency array. However, that would be incorrect. The purpose of useEffect() here is to set up the interval once, and once it’s set up, it will continue to run without needing to be reinitialized. If we included count as a dependency, the useEffect() would be called every time count changes, and since count is updated within useEffect(), this would trigger it again and again, resulting in an infinite loop.

Finally, we must clear the interval so that if the component unmounts, say, when the user navigates to a different page, the interval stops running and doesn’t continue in the background.

useRef()

useRef() is similar to useState() in that it allows you to persist values across different renders of the same component. However, the key difference is that useRef() does not trigger any re-renders when its value changes. This makes useRef() ideal for storing values that need to persist between renders, but don’t need to trigger re-renders (e.g., storing previous values, timers, or references to DOM elements).

The second primary way useRef() is used is to refer to DOM elements directly. While it’s generally discouraged to manipulate the DOM directly within React components (e.g., via getElementById() or querySelector()), React still allows for scenarios where you need to interact with the DOM for specific tasks like reading offsetWidth, scrollHeight, or focusing an input element. This is where useRef() comes in—it allows you to interact with DOM elements after the component has rendered, while still staying within React's declarative model.

For now, I won’t delve deeper into these use cases, but I’ll revisit them when I cover useRef() in more detail.

Application

Common uses include storing values that should persist between renders but shouldn’t trigger UI updates, like a previous state or counter. Or for storing a reference to a DOM element (e.g., to focus an input or get the size of an element).

Example

use-ref-example.jsx

1import React, { useState, useRef, useEffect } from 'react';
2
3function Timer() {
4  const [count, setCount] = useState(0);
5  const previousCountRef = useRef(0); // useRef for storing the previous count
6
7  // Update the previous count on every render
8  useEffect(() => {
9    previousCountRef.current = count;
10  }, [count]);
11
12  return (
13    <div>
14      <h1>Current count: {count}</h1>
15      <h2>Previous count: {previousCountRef.current}</h2>
16      <button onClick={() => setCount(count + 1)}>Increment</button>
17    </div>
18  );
19}
20

Explanation

We have a simple counter that increments the count each time the button is clicked. We use useRef() to store the previous value of the count (before the state updates). This allows us to display the previous count without triggering a re-render when previousCountRef.current is updated. The useEffect() hook updates the value of previousCountRef.current every time the count changes, but updating a useRef() does not trigger a re-render. As a result, the displayed previous count is always one step behind the current count.

Note: To change the value of a ref, you need to modify the .current property. Simply assigning a new value to the ref variable (e.g., previousCountRef = newValue) won’t work, because the ref itself is immutable. Instead, you should update the value using previousCountRef.current = newValue.