All posts
reactjavascriptfrontend

React Hooks Explained - No Jargon, Just Code

useState, useEffect, useRef - React hooks are everywhere. Let's demystify them with plain language and real examples.

Charles Agboh

Charles Agboh

May 6, 2025·3 min read

When React hooks landed in version 16.8, they changed everything. No more class components just to manage a bit of state. No more lifecycle method gymnastics. Hooks let you do all of that - and more - in simple, readable functions.

But if you're new to them, it can feel like there's a lot going on. Let's cut through the noise.

The Big Three

1. useState - Managing State

State is data that, when it changes, causes your component to re-render. useState is how you create it.

import { useState } from "react";

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

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

useState(0) gives you two things:

  • count - the current value
  • setCount - a function to update it

Every time you call setCount, React re-renders the component with the new value. Simple.

2. useEffect - Side Effects

Side effects are things that happen outside of rendering - fetching data, setting up timers, updating the document title, etc.

import { useState, useEffect } from "react";

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => setUser(data));
  }, [userId]); // runs whenever userId changes

  if (!user) return <p>Loading...</p>;
  return <p>Hello, {user.name}!</p>;
}

The second argument ([userId]) is the dependency array. The effect re-runs whenever any value in that array changes.

Watch out: passing an empty array [] means the effect runs once (on mount). Leaving it out means it runs after every render - usually not what you want.

3. useRef - Persistent Values Without Re-renders

useRef gives you a mutable box that persists across renders but doesn't cause re-renders when changed. Classic use case: DOM references.

import { useRef } from "react";

function FocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Type here..." />
      <button onClick={() => inputRef.current?.focus()}>Focus</button>
    </div>
  );
}

A Few Golden Rules

  1. Only call hooks at the top level - never inside if statements or loops
  2. Only call hooks from React functions - not regular JS functions
  3. Name your custom hooks starting with use - so React can recognise them

Building a Custom Hook

Once you get comfortable with the basics, you can compose hooks into your own custom ones. This is where things get powerful:

function useLocalStorage(key: string, initialValue: string) {
  const [value, setValue] = useState(
    () => localStorage.getItem(key) ?? initialValue
  );

  const setStored = (newValue: string) => {
    setValue(newValue);
    localStorage.setItem(key, newValue);
  };

  return [value, setStored] as const;
}

// Usage
const [theme, setTheme] = useLocalStorage("theme", "light");

Now you have a reusable piece of logic that any component can use. That's the real power of hooks - sharing logic without sharing UI.

You've Got the Foundation

Hooks aren't magic. They're just functions with a few special rules. Once useState, useEffect, and useRef feel natural, you'll be ready to explore useCallback, useMemo, useContext, and the rest of the family.

Take it one hook at a time. ⚛️

reactjavascriptfrontend

Found this helpful? Share it.

Charles Agboh

Written by

Charles Agboh

Builder, automator, and developer. Charlie writes about AI automation, Claude Code, n8n workflows, and the tools that actually move the needle for developers right now.

📬

Enjoyed this post?

Subscribe to get new posts straight to your inbox - no spam, just bytes.

Related Articles

Discussion

Loading...

Chat on WhatsApp