FOLASAYO SAMUEL OLAYEMI
SV_D_CURIOUS_SOFTWARE_ENGINEER'S BLOG

SV_D_CURIOUS_SOFTWARE_ENGINEER'S BLOG

How to perfectly use react-hooks.

Photo by Clark Tibbs on Unsplash

How to perfectly use react-hooks.

FOLASAYO SAMUEL OLAYEMI's photo
FOLASAYO SAMUEL OLAYEMI
·Mar 15, 2022·

10 min read

Subscribe to my newsletter and never miss my upcoming articles

Play this article

Table of contents

React-Hooks! What's special in using them?

Let's quickly explain how hooks work in React.

Hooks allow us to "hook" into React features such as state and lifecycle methods.

Hooks allow function components to have access to state and other React features. Because of this, class components are generally no longer needed.

Although Hooks generally replace class components, there are no plans to remove classes from React. Hooks were added to React in version 16.8.

There are 10 in-built hooks, and this is how you can perfectly use them in your project.

  • useState:

    The React useState Hook allows us to track state in a function component. State generally refers to data or properites that need to be tracking in an application.

To use the useState Hook, we first need to import it into our component at the top of the component. import { useState } from "react";

State can be initialized by calling useState in our function component.

useState accepts an initial state and returns two values which are the current state and a function that updates the state.

Always Initialize state at the top of the function component.

Code Sample of how to initialize state:

import { useState } from "react";

function FavoriteColor() {
  const [color, setColor] = useState("");
}

Note: We are destructuring the returned values from useState. The first value, color, is our current state. While, the second value, setColor, is the function that is used to update our state. We now set the initial state to an empty string useState("")

You can name the variables anything you would like.

Let's include our states anywhere by reading the state and using the state variable in the rendered component.

Code Sample:

import { useState } from "react";
import ReactDOM from "react-dom";

function FavoriteColor() {
  const [color, setColor] = useState("red");

  return <h1>My favorite color is {color}!</h1>
}

ReactDOM.render(<FavoriteColor />, document.getElementById('root'));

You can also update the state by using state updater function.

Note: Don't directly update a state.

Code Sample of how to update state:

import { useState } from "react";
import ReactDOM from "react-dom";

function FavoriteColor() {
  const [color, setColor] = useState("red");

  return (
    <>
      <h1>My favorite color is {color}!</h1>
      <button
        type="button"
        onClick={() => setColor("blue")}
      >Blue</button>
    </>
  )
}

ReactDOM.render(<FavoriteColor />, document.getElementById('root'));

useState Hook can be used to keep track of strings, numbers, booleans, arrays, objects, and any combination of these!

Code Sample:

import { useState } from "react";
import ReactDOM from "react-dom";

function Car() {
  const [car, setCar] = useState({
    brand: "Ford",
    model: "Mustang",
    year: "1964",
    color: "red"
  });

  return (
    <>
      <h1>My {car.brand}</h1>
      <p>
        It is a {car.color} {car.model} from {car.year}.
      </p>
    </>
  )
}

ReactDOM.render(<Car />, document.getElementById('root'));

You may be thinking that how can you update objects and arrays in state? No worries. We can use the JavaScript spread operator to help us get it done with ease.

Code Sample:

import { useState } from "react";
import ReactDOM from "react-dom";

function Car() {
  const [car, setCar] = useState({
    brand: "Ford",
    model: "Mustang",
    year: "1964",
    color: "red"
  });

  const updateColor = () => {
    setCar(previousState => {
      return { ...previousState, color: "blue" }
    });
  }

  return (
    <>
      <h1>My {car.brand}</h1>
      <p>
        It is a {car.color} {car.model} from {car.year}.
      </p>
      <button
        type="button"
        onClick={updateColor}
      >Blue</button>
    </>
  )
}

ReactDOM.render(<Car />, document.getElementById('root'));

Because we need the current value of state, we pass a function into our setCar function. This function receives the previous value. We then return an object, spreading the previousState and overwriting only the color. I believe this will help and you can also drop a comment if you want to know more about JavaScript spread operator.

  • useEffect:

The useEffect Hook allows you to perform side effects in your components. Some examples of side effects are: fetching data, directly updating the DOM, and timers.

useEffect accepts two arguments. The second argument is optional. Example: useEffect(<function>, <dependency>)

Code Sample:

import { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function Counter() {
  const [count, setCount] = useState(0);
  const [calculation, setCalculation] = useState(0);

  useEffect(() => {
    setCalculation(() => count * 2);
  }, [count]); // <- add the count variable here

  return (
    <>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
      <p>Calculation: {calculation}</p>
    </>
  );
}

ReactDOM.render(<Counter />, document.getElementById("root"));
  • useMemo:

The React useMemo Hook returns a memoized value. Memoization is just like caching a value so that it does not need to be recalculated.

The useMemo Hook only runs when one of its dependencies update.

The useMemo and useCallback Hooks are similar. The main difference is that useMemo returns a memoized value and, useCallback returns a memoized function.

Note: The useMemo Hook can be used to keep expensive, resource intensive functions from needlessly running.

Code Sample:

import { useState, useMemo } from "react";
import ReactDOM from "react-dom";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);
  const calculation = useMemo(() => expensiveCalculation(count), [count]);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = () => {
    setTodos((t) => [...t, "New Todo"]);
  };

  return (
    <div>
      <div>
        <h2>My Todos</h2>
        {todos.map((todo, index) => {
          return <p key={index}>{todo}</p>;
        })}
        <button onClick={addTodo}>Add Todo</button>
      </div>
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
        <h2>Expensive Calculation</h2>
        {calculation}
      </div>
    </div>
  );
};

const expensiveCalculation = (num) => {
  console.log("Calculating...");
  for (let i = 0; i < 1000000000; i++) {
    num += 1;
  }
  return num;
};

ReactDOM.render(<App />, document.getElementById("root"));
  • useContext:

    React Context is a way to manage state globally. It can be used together with the useState Hook to share state between deeply nested components more easily than with useState alone.

Note: State should be held by the highest parent component in the stack that requires access to the state.

Code Sample:

import { useState, createContext, useContext } from "react";
import ReactDOM from "react-dom";

const UserContext = createContext();

function Component1() {
  const [user, setUser] = useState("Jesse Hall");

  return (
    <UserContext.Provider value={user}>
      <h1>{`Hello ${user}!`}</h1>
      <Component2 user={user} />
    </UserContext.Provider>
  );
}

function Component2() {
  return (
    <>
      <h1>Component 2</h1>
      <Component3 />
    </>
  );
}

function Component3() {
  return (
    <>
      <h1>Component 3</h1>
      <Component4 />
    </>
  );
}

function Component4() {
  return (
    <>
      <h1>Component 4</h1>
      <Component5 />
    </>
  );
}

function Component5() {
  const user = useContext(UserContext);

  return (
    <>
      <h1>Component 5</h1>
      <h2>{`Hello ${user} again!`}</h2>
    </>
  );
}

ReactDOM.render(<Component1 />, document.getElementById("root"));
  • useCallback:

The React useCallback Hook returns a memoized callback function. Memoization is just like caching a value so that it does not need to be recalculated.

This allows us to isolate resource intensive functions so that they will not automatically run on every render. The useCallback Hook only runs when one of its dependencies update.

The useCallback and useMemo Hooks are similar. The main difference is that useMemo returns a memoized value and useCallback returns a memoized function.

Note: One reason to use useCallback is to prevent a component from re-rendering unless its props have changed.

Code Sample:

import { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import Todos from "./Todos";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = useCallback(() => {
    setTodos((t) => [...t, "New Todo"]);
  }, [todos]);

  return (
    <>
      <Todos todos={todos} addTodo={addTodo} />
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
      </div>
    </>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
  • useReducer:

If you find yourself keeping track of multiple pieces of state that rely on complex logic, useReducer may be useful. It allows for custom state logic.

The useReducer Hook is similar to the useState Hook.

The useReducer Hook accepts two arguments.

useReducer(<reducer>, <initialState>)

The reducer function contains your custom state logic and the initialState can be a simple value but generally will contain an object.

The useReducer Hook returns the current state and a dispatch method.

Sample Code:

import { useReducer } from "react";
import ReactDOM from "react-dom";

const initialTodos = [
  {
    id: 1,
    title: "Todo 1",
    complete: false,
  },
  {
    id: 2,
    title: "Todo 2",
    complete: false,
  },
];

const reducer = (state, action) => {
  switch (action.type) {
    case "COMPLETE":
      return state.map((todo) => {
        if (todo.id === action.id) {
          return { ...todo, complete: !todo.complete };
        } else {
          return todo;
        }
      });
    default:
      return state;
  }
};

function Todos() {
  const [todos, dispatch] = useReducer(reducer, initialTodos);

  const handleComplete = (todo) => {
    dispatch({ type: "COMPLETE", id: todo.id });
  };

  return (
    <>
      {todos.map((todo) => (
        <div key={todo.id}>
          <label>
            <input
              type="checkbox"
              checked={todo.complete}
              onChange={() => handleComplete(todo)}
            />
            {todo.title}
          </label>
        </div>
      ))}
    </>
  );
}

ReactDOM.render(<Todos />, document.getElementById("root"));
  • useRef:

The useRefHook allows you to persist values between renders. It can be used to store a mutable value that does not cause a re-render when updated. It can be used to access a DOM element directly.

If we tried to count how many times our application renders using the useState Hook, we would be caught in an infinite loop since this Hook itself causes a re-render. To avoid this, we can use the useRef Hook.

useRef hook does not cause re-render.

Code Sample:

import { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";

function App() {
  const [inputValue, setInputValue] = useState("");
  const previousInputValue = useRef("");

  useEffect(() => {
    previousInputValue.current = inputValue;
  }, [inputValue]);

  return (
    <>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <h2>Current Value: {inputValue}</h2>
      <h2>Previous Value: {previousInputValue.current}</h2>
    </>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
  • useDebugValue:

It allows us to define our own custom labels in React Dev Tools, which are useful for debugging.

Code Sample:

function useDisplayName() {
    const [displayName, setDisplayName] = React.useState();

    React.useEffect(() => {
        const data = fetchFromDatabase(props.userId);
        setDisplayName(data.displayName);
    }, []);

    React.useDebugValue(displayName ?? 'loading...');
    return displayName;
}
  • useImperativeHandle:

This hook is use to modify the exposed ref and it is rarely used.

Code Sample:

function useImperativeHandleDemo(props, ref) {

    const myBtn = useRef(null);

    React.useImperativeHandle(ref, () => ({
        click: () => {
            console.log('clicking button!');
            myBtn.current.click();
        }
    }));
}
  • useLayoutEffect:

It works the same as useEffect hook with one difference, the callback will run after rendering the component but before the actual updates have been painted to the screen. Blocks visual updates until your callback is finished.

Code Sample:

function useLayoutEffectDemo() {

    const myBtn = React.useRef(null);

    React.useLayoutEffect(() => {
        const rect = myBtn.current.getBoundingClientRect();
        // scroll position before the dom is visually updated
        console.log(rect.height);
    })
}

###How To Use React Custom Hooks

When you have component logic that needs to be used by multiple components, we can extract that logic to a custom Hook. Custom Hooks start with "use". Example: useFetch.

Hooks are reusable functions.

Let's build a hook. In the following code, we are fetching data in our Home component and displaying it.

We will use the JSONPlaceholder service to fetch fake data. This service is great for testing applications when there is no existing data.

Code Sample:

Place this code below in your index.js.

import { useState, useEffect } from "react";
import ReactDOM from "react-dom";

const Home = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/todos")
      .then((res) => res.json())
      .then((data) => setData(data));
 }, []);

  return (
    <>
      {data &&
        data.map((item) => {
          return <p key={item.id}>{item.title}</p>;
        })}
    </>
  );
};

ReactDOM.render(<Home />, document.getElementById("root"));

The fetch logic may be needed in other components as well, so we will extract that into a custom Hook.

Now, move the fetch logic to a new file to be used as a custom Hook:

Create a new file and name it useFetch or anything you like, then place this code below:

Code Sample:

import { useState, useEffect } from "react";

const useFetch = (url) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((data) => setData(data));
  }, [url]);

  return [data];
};

export default useFetch;

In your index.js file now import the useFetch file. Check the sample below for better understanding:

import ReactDOM from "react-dom";
import useFetch from "./useFetch";

const Home = () => {
  const [data] = useFetch("https://jsonplaceholder.typicode.com/todos");

  return (
    <>
      {data &&
        data.map((item) => {
          return <p key={item.id}>{item.title}</p>;
        })}
    </>
  );
};

ReactDOM.render(<Home />, document.getElementById("root"));

Let's make a better explanation to the code above:

Firstly, we have created a new file called useFetch.js containing a function called useFetch which contains all of the logic needed to fetch our data.

We now removed the hard-coded URL and replaced it with a url variable that can be passed to the custom Hook.

Lastly, we are returning our data from our Hook.

In index.js, we are importing our useFetch Hook and utilizing it like any other Hook. This is where we pass in the URL to fetch data from.

Now we can reuse this custom Hook in any component to fetch data from any URL.

Did you find this article valuable?

Support FOLASAYO SAMUEL OLAYEMI by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors
 
Share this