Custom Hooks in React

Creating Custom Hooks in React — Reusable Logic Made Easy

So far we’ve learnt Hooks, useState, useEffect and useRef. Custom hooks let you extract reusable logic from React components, making your code cleaner and easier to maintain. In this tutorial, we’ll create a useLocalStorage hook to persist tasks in our Task Tracker App, so they won’t disappear when the page reloads. You’ll also learn best practices for writing your own reusable hooks.

So, follow along in this tutorial.

What Are Custom Hooks?

A custom hook is just a function that:

  1. Starts with the word use
  2. Uses one or more React hooks inside it (useState, useEffect, etc.)

Custom hooks let you extract and reuse logic across multiple components.

Example: Instead of writing the same localStorage logic in 5 places, you can build one useLocalStorage hook and use it everywhere.

What You’ll Learn in This Post

  • How to create a useLocalStorage hook
  • How to integrate useLocalStorage into the Task Tracker App
  • Best practices for writing reusable hooks

Example: A Simple Custom Hook

import { useState, useEffect } from "react";

// Persist state in localStorage
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    // Load from localStorage or use default
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

export default useLocalStorage;

This hook works just like useState, but it automatically syncs with localStorage.

Mini Project Step: Persist Tasks with useLocalStorage

Now let’s plug this hook into our Task Tracker App.

1. Create useLocalStorage.js inside a hooks/ folder

// src/hooks/useLocalStorage.js
import { useState, useEffect } from "react";

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

export default useLocalStorage;

2. Update App.jsx to use the hook

import React, { useState, useEffect, useRef } from "react";
import useLocalStorage from "./hooks/useLocalStorage";

function App() {
  const [tasks, setTasks] = useLocalStorage("tasks", []);
  const [newTask, setNewTask] = useState("");
  const inputRef = useRef(null);

  // Fetch initial tasks only if localStorage is empty
  useEffect(() => {
    if (tasks.length === 0) {
      fetch("https://jsonplaceholder.typicode.com/todos?_limit=5")
        .then((res) => res.json())
        .then((data) => setTasks(data))
        .catch((err) => console.error("Error fetching tasks:", err));
    }
  }, []); // run once on mount

  // Auto-focus the input on mount
  useEffect(() => {
    inputRef.current.focus();
  }, []);

  const addTask = (e) => {
    e.preventDefault();
    if (!newTask.trim()) return;

    const task = { id: Date.now(), title: newTask };
    setTasks([...tasks, task]);
    setNewTask("");
    inputRef.current.focus(); // Re-focus after adding
  };

  return (
    <div style={{ padding: "20px" }}>
      <h1>Task Tracker</h1>

      {/* Form to add tasks */}
      <form onSubmit={addTask}>
        <input
          ref={inputRef}
          type="text"
          placeholder="Enter new task"
          value={newTask}
          onChange={(e) => setNewTask(e.target.value)}
        />
        <button type="submit">Add Task</button>
      </form>

      {/* Render task list */}
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>{task.title || task.text}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Now your tasks persist in localStorage — they won’t disappear on page refresh!

Common Mistakes with Custom Hooks

  1. Forgetting the use prefix
    • Must start with use or React won’t treat it as a hook.
  2. Adding too much logic
    • Keep hooks focused. Write small, reusable ones.
  3. Not handling JSON properly in localStorage
    • Always JSON.stringify when saving, JSON.parse when loading.

What We’ve Achieved

  • Learned how to write a custom hook
  • Built a useLocalStorage hook to persist data
  • Upgraded our Task Tracker App so tasks survive refreshes

Learning Path

Post 1: Introduction to React Hooks (Why They Matter + Starter Project) – Read Post-1

Learn what hooks are, why they replaced class components, and set up the starter Task Tracker App.

Post 2: Mastering useState in React – Read Post 2

Manage state in functional components. Add new tasks dynamically with useState.

Post 3: Understanding useEffect (Side Effects + API Calls) – Read Post 3

Fetch tasks from an API, mimic lifecycle methods, and clean up side effects.

Post 4: Using useRef in React (Not Just for DOM Access) – Read Post 4

Control focus on the input field and learn how to store values without re-renders.

Post 5: Creating Custom Hooks (useLocalStorage) – Current Post

Extract reusable logic into a custom hook that persists tasks across refreshes.

Post 6: Optimizing Performance with useMemo and useCallbackRead Post 6

Add a search feature and optimize it with memoization to prevent unnecessary re-renders.

Post 7 (Final): Building a Complete Task Tracker with React Hooks – Read Post 7

Add delete + toggle complete functionality, and wrap up with a fully functional app.


Discover more from TACETRA

Subscribe to get the latest posts sent to your email.

Let's have a discussion!

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Discover more from TACETRA

Subscribe now to keep reading and get access to the full archive.

Continue reading