React Hooks Tutorial for Beginners: Build Your First Hook-Based App

By
On:

If you’re learning React, you’ve probably heard about React Hooks and for good reason. Hooks completely changed how we write React components by making it easier to reuse logic and manage state without dealing with complex class components. Whether you’re new to React or coming from class components, this guide will walk you through everything you need to know to build your first hook-based app.

What Are React Hooks? (And Why Should You Care?)

Before React 16.8, state management was only possible in class components. If you wanted to use state in a functional component, you were out of luck. Then came Hooks a revolutionary feature that lets you use state and other React features directly in functional components.

Think of Hooks as special functions that “hook into” React features. They’re optional, backward-compatible, and they make your code cleaner and easier to maintain. The best part? They’re actually fun to use once you understand them.

The Two Essential Hooks You Need to Know

1. useState: Your Component’s Memory

The useState hook is the bread and butter of React Hooks. It allows you to add state to your functional component, giving it “memory” that persists between renders.

Here’s how it works:

import React, { useState } from 'react';

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Let me break this down:

  • const [count, setCount] = useState(0); — This is destructuring. count holds your current state value, and setCount is the function you use to update it. The 0 is your initial state.

  • count — This is your state variable. It starts at 0.

  • setCount — This is the function that updates count. When you call setCount(5), React knows to re-render your component with the new value.

Why this matters: When the user clicks the button, setCount updates the state, React re-renders the component, and the count increases on screen. That’s the magic of useState.

Using Multiple State Variables:

You can call useState multiple times in a single component. Each one gets its own independent state:

function UserForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState('');

  return (
    <div>
      <input 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
        placeholder="Enter name"
      />
      <input 
        value={email} 
        onChange={(e) => setEmail(e.target.value)} 
        placeholder="Enter email"
      />
      <input 
        value={age} 
        onChange={(e) => setAge(e.target.value)} 
        placeholder="Enter age"
      />
    </div>
  );
}

2. useEffect: Side Effects Made Simple

While useState manages state, useEffect handles side effects—things like fetching data from an API, updating the page title, or subscribing to events. Side effects are actions that happen outside your component’s normal rendering flow.

import React, { useState, useEffect } from 'react';

function Greeting() {
  const [name, setName] = useState('World');

  useEffect(() => {
    document.title = `Hello, ${name}!`;
  }, [name]);

  return (
    <div>
      <h1>Hello, {name}!</h1>
      <input 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
      />
    </div>
  );
}

Here’s what’s happening:

  • The effect function — This is the code inside useEffect(() => {...}). It runs after React renders your component.

  • The dependency array — That [name] at the end tells React when to run the effect. In this case, it runs after every render where name changes. If you leave it empty [], the effect runs only once after the first render. If you remove the array entirely, it runs after every render.

A Real-World Example: Fetching Data

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (error) {
        console.error('Error fetching user:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  if (!user) return <p>No user found</p>;

  return <div>{user.name}</div>;
}

This component fetches a user from an API when the component mounts or when userId changes. The loading state keeps track of whether the data is still being fetched.

Building Your First Hook-Based App: A Todo List

Now let’s bring it all together and build a simple todo app—the perfect project to understand Hooks in action.

Step 1: Set Up Your React Project

First, create a new React app:

npx create-react-app my-todo-app
cd my-todo-app
npm start

Step 2: Create the Todo Component

Replace the contents of src/App.js with this:

import React, { useState } from 'react';
import './App.css';

function App() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React Hooks', completed: false },
    { id: 2, text: 'Build a Todo App', completed: false },
    { id: 3, text: 'Master useEffect', completed: false }
  ]);
  const [input, setInput] = useState('');

  // Add a new todo
  const addTodo = () => {
    if (input.trim() === '') return;

    const newTodo = {
      id: Date.now(),
      text: input,
      completed: false
    };

    setTodos([...todos, newTodo]);
    setInput('');
  };

  // Toggle todo completion
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  // Delete a todo
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div className="app">
      <h1>My Todo List</h1>
      
      <div className="input-container">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
          placeholder="Add a new todo..."
        />
        <button onClick={addTodo}>Add</button>
      </div>

      <ul className="todo-list">
        {todos.map((todo) => (
          <li key={todo.id} className={todo.completed ? 'completed' : ''}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span>{todo.text}</span>
            <button 
              className="delete-btn"
              onClick={() => deleteTodo(todo.id)}
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Step 3: Add Some Basic Styling

Replace src/App.css with:

.app {
  max-width: 600px;
  margin: 50px auto;
  font-family: Arial, sans-serif;
}

h1 {
  color: #333;
  text-align: center;
}

.input-container {
  display: flex;
  gap: 10px;
  margin-bottom: 30px;
}

.input-container input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

.input-container button {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

.input-container button:hover {
  background-color: #0056b3;
}

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-list li {
  display: flex;
  align-items: center;
  padding: 15px;
  border: 1px solid #eee;
  border-radius: 4px;
  margin-bottom: 10px;
  background-color: #f9f9f9;
}

.todo-list li.completed {
  opacity: 0.6;
}

.todo-list li.completed span {
  text-decoration: line-through;
  color: #999;
}

.todo-list input[type="checkbox"] {
  width: 20px;
  height: 20px;
  margin-right: 15px;
  cursor: pointer;
}

.todo-list span {
  flex: 1;
  font-size: 16px;
}

.delete-btn {
  padding: 5px 10px;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.delete-btn:hover {
  background-color: #c82333;
}

Step 4: Test Your App

Visit http://localhost:3000 and you’ll see your working todo app! Try adding todos, marking them complete, and deleting them. Everything works thanks to useState managing your component’s state.

Understanding the Todo App

Let’s understand what’s happening under the hood:

State Management:

  • todos — An array holding all your todo items. Each todo has an idtext, and completed status.

  • input — Stores what the user is typing in the input field.

State Updates:

  • addTodo() — Creates a new todo object with Date.now() as a unique ID, adds it to the todos array using the spread operator [...todos, newTodo], and clears the input.

  • toggleTodo(id) — Uses .map() to find the matching todo and flip its completed status while keeping others unchanged.

  • deleteTodo(id) — Uses .filter() to remove the todo with the matching ID.

Why This Pattern Works:
Notice how we never modify state directly. Instead, we create new arrays and objects. This is crucial because React compares the old and new state to know when to re-render. If you mutated the state directly, React might not detect the change.

Important Rules When Using Hooks

Before you start building more complex apps, know these critical rules:

  1. Only call Hooks at the top level — Don’t call Hooks inside loops, conditions, or nested functions. Always call them at the top level of your component.

// ✅ Correct
function MyComponent() {
  const [count, setCount] = useState(0);
  // ... rest of code
}

// ❌ Wrong
function MyComponent() {
  if (someCondition) {
    const [count, setCount] = useState(0); // Don't do this!
  }
}
  1. Only call Hooks from React functions — Call Hooks only from React functional components or custom Hooks, not from regular JavaScript functions.

Next Steps: Explore More Hooks

Once you’re comfortable with useState and useEffect, React has several other built-in Hooks worth exploring:

  • useContext — Share data between components without passing props at every level

  • useReducer — Manage complex state logic similar to Redux

  • useRef — Access DOM elements directly or keep values that don’t trigger re-renders

  • useCallback and useMemo — Optimize performance by memoizing functions and values

Common Beginner Mistakes to Avoid

1. Forgetting the dependency array in useEffect

// This runs after EVERY render (probably not what you want)
useEffect(() => {
  fetchData();
});

// This runs only once after mounting
useEffect(() => {
  fetchData();
}, []);

// This runs when 'userId' changes
useEffect(() => {
  fetchData(userId);
}, [userId]);

2. Mutating state directly

// ❌ Wrong
const [count, setCount] = useState(0);
count = count + 1; // Don't do this!

// ✅ Correct
const [count, setCount] = useState(0);
setCount(count + 1);

3. Creating new objects/arrays without reason

// If you have an object state
const [user, setUser] = useState({ name: 'John', age: 30 });

// ❌ Create new object unnecessarily
setUser({ name: 'John', age: 30 }); // This causes re-render even though nothing changed

// ✅ Only update what changed
setUser({ ...user, age: 31 });

Wrapping Up

React Hooks transformed the way developers write React components. With useState and useEffect, you can build fully functional apps without ever touching class components. The todo app you just built is a perfect starting point—it demonstrates state management, event handling, and component updates.

Read also: How to create a forever loop in JavaScript

Santhakumar Raja

I am the founder of Pedagogy Zone, a dedicated education platform that provides reliable and up-to-date information on academic trends, learning resources, and educational developments.

For Feedback - techactive6@gmail.com