Other Hooks in React

React provides several Hooks beyond the commonly used Hooks such as useState, useEffect, useContext, useRef, useMemo, and useCallback.

Some of these additional Hooks solve very specific problems.

For example:

  • useId helps generate unique IDs for accessibility.
  • useDebugValue helps developers inspect custom Hooks during development.
  • useSyncExternalStore allows React components to subscribe to external stores safely.
  • useEffectEvent helps separate non-reactive logic from Effects.
  • useActionState helps manage the result and pending state of Actions.
  • useOptimistic helps create responsive optimistic user interfaces.

You will not use every one of these Hooks in every application.

Some are especially useful when building reusable libraries, advanced components, forms, or applications that interact with external state systems.

Understanding these Hooks gives you a more complete picture of the React Hooks ecosystem.

useId

useId is a React Hook for generating unique IDs that can be used to connect related elements.

Import it from React:

import { useId } from "react";

The basic syntax is:

const id = useId();

React generates an ID that is stable for that component instance.

Why Does React Need useId?

Consider an accessible form field.

<label htmlFor="email">
  Email
</label>

<input id="email" />

The label is connected to the input through:

htmlFor="email"

and:

id="email"

This works for one component.

But imagine rendering the same component multiple times:

<EmailField />
<EmailField />
<EmailField />

If every component uses the same hard-coded ID:

id="email"

you can end up with duplicate IDs in the page.

useId solves this problem.

Basic useId Example

import { useId } from "react";

function EmailField() {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>
        Email
      </label>

      <input
        id={id}
        type="email"
      />
    </div>
  );
}

Each instance of EmailField receives its own generated ID.

Using useId for Accessibility

useId is especially useful when connecting form controls to descriptive elements.

For example:

function PasswordField() {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>
        Password
      </label>

      <input
        id={id}
        type="password"
        aria-describedby={`${id}-description`}
      />

      <p id={`${id}-description`}>
        Your password must contain at least 8 characters.
      </p>
    </div>
  );
}

Now the input is associated with both:

  • Its label
  • Its description

This can improve accessibility for users of assistive technologies.

Generating Multiple Related IDs

A single useId value can be used as the base for several related IDs.

const id = useId();

const inputId = `${id}-input`;
const descriptionId = `${id}-description`;
const errorId = `${id}-error`;

Then:

<label htmlFor={inputId}>
  Username
</label>

<input
  id={inputId}
  aria-describedby={descriptionId}
  aria-errormessage={errorId}
/>

This allows all related elements to have unique IDs.

Do Not Use useId for List Keys

A common mistake is using useId as a key:

items.map((item) => (
  <li key={useId()}>
    {item.name}
  </li>
))

This is not the correct use of useId.

List keys should come from stable data associated with the item:

items.map((item) => (
  <li key={item.id}>
    {item.name}
  </li>
))

useId is designed primarily for generating IDs that connect related elements.

Do Not Use useId for Application Data

Avoid using useId as:

  • A database ID
  • A product ID
  • An order ID
  • A random identifier for business logic
  • A replacement for backend-generated IDs

For example:

const userId = useId();

does not mean the user has been assigned a real application user ID.

useId is primarily about unique identifiers in the rendered React tree.

useDebugValue

useDebugValue is a Hook used by custom Hooks to display useful information in React Developer Tools.

Import it:

import { useDebugValue } from "react";

The basic syntax is:

useDebugValue(value);

It does not change the application’s UI.

Instead, it helps developers understand what a custom Hook is doing while debugging.

Why Use useDebugValue?

Imagine you create a custom Hook:

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);

  return isOnline;
}

When inspecting this Hook in React Developer Tools, it can be useful to see something more descriptive than an internal state value.

You can write:

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);

  useDebugValue(
    isOnline ? "Online" : "Offline"
  );

  return isOnline;
}

React Developer Tools can then display a more meaningful value.

useDebugValue With a Custom Hook

For example:

function useUser(user) {
  useDebugValue(
    user ? user.name : "Not logged in"
  );

  return user;
}

This makes the custom Hook easier to inspect during development.

Formatting useDebugValue

You can also provide a formatting function:

useDebugValue(
  user,
  (user) => user
    ? `User: ${user.name}`
    : "No user"
);

The formatting function can help keep potentially expensive formatting work out of the normal rendering path until the value is inspected by development tools.

Should Every Custom Hook Use useDebugValue?

No.

Most custom Hooks do not need it.

It is particularly useful for:

  • Reusable libraries
  • Complex custom Hooks
  • Hooks with non-obvious internal state
  • Development and debugging

For a simple Hook, adding useDebugValue may provide little benefit.

useSyncExternalStore

useSyncExternalStore is an advanced Hook for subscribing to a data source that exists outside React.

Import it:

import { useSyncExternalStore } from "react";

The basic syntax is:

const value = useSyncExternalStore(
  subscribe,
  getSnapshot
);

It can also accept a third argument for the server-rendered snapshot:

const value = useSyncExternalStore(
  subscribe,
  getSnapshot,
  getServerSnapshot
);

What Is an External Store?

An external store is state that React does not directly own.

Examples include:

  • Browser APIs
  • Third-party state libraries
  • Custom JavaScript stores
  • Shared application stores
  • External data sources

For example:

External Store
      ↓
subscribe()
      ↓
React Component
      ↓
UI

Why Does useSyncExternalStore Exist?

React needs a safe way to subscribe to external data sources while maintaining consistent rendering behavior.

Instead of manually combining:

useEffect(...)
useState(...)

for an external store, React provides useSyncExternalStore.

This is particularly important for library authors and advanced state-management implementations.

A Simple External Store

Imagine a small JavaScript store:

let count = 0;

const listeners = new Set();

function subscribe(listener) {
  listeners.add(listener);

  return () => {
    listeners.delete(listener);
  };
}

function getSnapshot() {
  return count;
}

function increment() {
  count += 1;

  listeners.forEach((listener) => {
    listener();
  });
}

A React component can subscribe to it:

function Counter() {
  const count = useSyncExternalStore(
    subscribe,
    getSnapshot
  );

  return (
    <button onClick={increment}>
      Count: {count}
    </button>
  );
}

React reads the snapshot and subscribes to changes.

When the external store changes, React can update the component.

The subscribe Function

The subscribe function tells React how to subscribe to changes.

function subscribe(listener) {
  listeners.add(listener);

  return () => {
    listeners.delete(listener);
  };
}

The returned function is used to unsubscribe.

This follows the same general setup and cleanup idea seen with Effects.

The getSnapshot Function

getSnapshot tells React what the current external value is.

function getSnapshot() {
  return count;
}

React calls this function when it needs the current value.

The snapshot should be stable until the underlying store changes.

Server Rendering and getServerSnapshot

Applications that render React on the server can provide:

getServerSnapshot

For example:

const value = useSyncExternalStore(
  subscribe,
  getSnapshot,
  getServerSnapshot
);

This gives React a value it can use during server rendering.

This is particularly important for applications that support server rendering or hydration.

When Should You Use useSyncExternalStore?

Most application developers will not need to use this Hook directly every day.

It is most relevant when:

  • Building a state-management library
  • Integrating an external store
  • Creating reusable infrastructure
  • Subscribing to external state
  • Supporting server rendering with external data

If your state can comfortably live in React with useState, useReducer, and Context, those tools are often simpler.

useEffectEvent

useEffectEvent is designed for a specific Effect problem: an Effect may need to react to certain values while also reading other values without making those other values reactive dependencies.

Import it:

import { useEffectEvent } from "react";

The Hook creates an Effect Event.

For example:

const onConnected = useEffectEvent(() => {
  showNotification(theme);
});

The Effect Event can read the latest values without causing the surrounding Effect to re-synchronize simply because those values changed.

Why Are Effect Events Useful?

Consider a chat application.

Suppose a connection depends on:

roomId

but when the connection succeeds, you want to display a notification using the current:

theme

A naive Effect might be:

useEffect(() => {
  const connection = createConnection(roomId);

  connection.on("connected", () => {
    showNotification(
      "Connected!",
      theme
    );
  });

  connection.connect();

  return () => {
    connection.disconnect();
  };
}, [roomId, theme]);

Now changing the theme also causes the connection to be disconnected and recreated.

But the connection itself may not depend on the theme.

Separating Reactive and Non-Reactive Logic

With an Effect Event:

const onConnected = useEffectEvent(() => {
  showNotification(
    "Connected!",
    theme
  );
});

useEffect(() => {
  const connection = createConnection(roomId);

  connection.on("connected", onConnected);

  connection.connect();

  return () => {
    connection.disconnect();
  };
}, [roomId]);

Now:

roomId
  ↓
controls connection

theme
  ↓
used by Effect Event

Changing the theme does not cause the connection Effect to reconnect.

Why Not Just Remove theme From the Dependency Array?

Simply removing a dependency can create stale-value problems.

For example:

useEffect(() => {
  console.log(theme);
}, []);

The Effect may continue using the value captured by that render.

useEffectEvent provides a specific mechanism for non-reactive logic that needs access to current values.

Important Rules for Effect Events

Effect Events are intended for use from Effects and are not general-purpose event handlers.

They should not be treated as a replacement for:

onClick

or other user interaction handlers.

The purpose is to separate non-reactive logic inside an Effect from the Effect’s reactive dependencies.

useActionState

useActionState is a Hook designed for managing the result and pending state of an Action.

Import it:

import { useActionState } from "react";

The basic syntax is:

const [state, formAction, isPending] =
  useActionState(
    action,
    initialState
  );

There are three important values:

state
   ↓
Result of the latest Action

formAction
   ↓
Function used to trigger the Action

isPending
   ↓
Whether the Action is currently running

Why Use useActionState?

Forms often need to manage:

  • Submission
  • Validation
  • Success messages
  • Error messages
  • Pending states
  • Server responses

Without an Action-based approach, you may write several pieces of state manually.

For example:

const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");

useActionState provides a structured way to manage the result of an Action.

Basic useActionState Example

import { useActionState } from "react";

async function subscribeAction(
  previousState,
  formData
) {
  const email = formData.get("email");

  if (!email) {
    return {
      error: "Email is required",
    };
  }

  return {
    success: "Successfully subscribed!",
  };
}

function NewsletterForm() {
  const [
    state,
    formAction,
    isPending,
  ] = useActionState(
    subscribeAction,
    {}
  );

  return (
    <form action={formAction}>
      <input
        name="email"
        type="email"
        placeholder="Email address"
      />

      <button disabled={isPending}>
        {isPending
          ? "Subscribing..."
          : "Subscribe"}
      </button>

      {state.error && (
        <p>{state.error}</p>
      )}

      {state.success && (
        <p>{state.success}</p>
      )}
    </form>
  );
}

The Action receives:

previousState

and:

formData

The Action returns the next state.

useActionState Flow

The process can be understood as:

User submits form
        ↓
formAction
        ↓
Action runs
        ↓
Action returns state
        ↓
state updates
        ↓
UI displays result

While the Action is running:

isPending

can be used to provide feedback.

useActionState With Validation

Actions can return structured validation errors.

async function loginAction(
  previousState,
  formData
) {
  const email = formData.get("email");
  const password = formData.get("password");

  if (!email) {
    return {
      error: "Email is required",
    };
  }

  if (!password) {
    return {
      error: "Password is required",
    };
  }

  return {
    success: "Login successful",
  };
}

The component can display the result:

{state.error && (
  <p>{state.error}</p>
)}

{state.success && (
  <p>{state.success}</p>
)}

useActionState and Pending UI

The third value:

isPending

makes it easy to show a pending state:

<button disabled={isPending}>
  {isPending ? "Saving..." : "Save"}
</button>

This improves the user experience because the interface communicates that the operation is in progress.

useActionState With Server Actions

In React applications using frameworks that support server-side Actions, useActionState can work with Actions that run on the server.

The exact implementation depends on the framework.

The important React concept is that the Action can receive form data, perform work, and return a state value that the component displays.

useOptimistic

useOptimistic is used to create optimistic UI updates.

An optimistic update shows the expected result immediately instead of waiting for a server request to finish.

For example, when a user submits a comment:

User submits comment
       ↓
Show comment immediately
       ↓
Send request
       ↓
Server responds
       ↓
Confirm or handle failure

This can make applications feel much faster.

Basic useOptimistic Syntax

Import it:

import { useOptimistic } from "react";

The basic pattern is:

const [optimisticState, addOptimistic] =
  useOptimistic(
    state,
    updateFunction
  );

The exact update function depends on the type of optimistic state you are managing.

Optimistic Comment Example

Suppose we have:

const [comments, setComments] = useState([]);

We can create an optimistic version:

const [optimisticComments, addOptimisticComment] =
  useOptimistic(
    comments,
    (currentComments, newComment) => [
      ...currentComments,
      {
        ...newComment,
        sending: true,
      },
    ]
  );

When submitting:

addOptimisticComment({
  id: crypto.randomUUID(),
  text,
});

The new comment can immediately appear in the UI with:

sending: true

while the actual operation is still being processed.

Displaying Optimistic State

For example:

<ul>
  {optimisticComments.map((comment) => (
    <li key={comment.id}>
      {comment.text}

      {comment.sending && (
        <span> Sending...</span>
      )}
    </li>
  ))}
</ul>

The user sees the comment immediately.

Optimistic Updates and Failure

Optimistic UI does not mean pretending an operation always succeeds.

The server still needs to confirm the operation.

If the request fails, the application needs to handle the failure and reconcile the UI with the actual state.

The flow is:

Optimistic UI
     ↓
Server request
     ↓
Success ─────→ Confirm
     │
     ↓
Failure
     ↓
Reconcile UI

This is especially important for actions such as:

  • Adding comments
  • Sending messages
  • Liking a post
  • Updating a setting
  • Adding an item to a cart
  • Reordering items

useOptimistic With a Form Action

useOptimistic can be especially useful with Actions.

Conceptually:

const [
  optimisticComments,
  addOptimisticComment,
] = useOptimistic(
  comments,
  (currentComments, comment) => [
    ...currentComments,
    comment,
  ]
);

Then a form submission can immediately add the optimistic comment before the server operation finishes.

Optimistic UI Requires Care

Optimistic updates work best when the expected result is predictable.

For example:

Like button

is often straightforward.

But operations with complicated server-side validation or unpredictable outcomes require more careful reconciliation.

Do not assume that an optimistic update is always correct.

Comparing the Hooks

These Hooks solve very different problems.

HookMain Purpose
useIdGenerate unique IDs for related elements
useDebugValueDisplay custom Hook information in React Developer Tools
useSyncExternalStoreSubscribe to external stores safely
useEffectEventSeparate non-reactive logic from Effects
useActionStateManage Action results and pending state
useOptimisticDisplay optimistic UI while an operation is pending

Choosing the Right Hook

A simple decision process can help.

Need a unique DOM ID?

Use:

useId()

Building a custom Hook that is difficult to inspect?

Consider:

useDebugValue()

Connecting React to an external state store?

Consider:

useSyncExternalStore()

An Effect contains non-reactive logic that needs current values?

Consider:

useEffectEvent()

Managing the result of a form or Action?

Consider:

useActionState()

Want the UI to respond immediately while an Action is processing?

Consider:

useOptimistic()

Common Mistakes With useId

Do not use useId as a list key.

Do not use it as a database identifier.

Do not generate business identifiers with it.

Use it for relationships between DOM elements.

Common Mistakes With useDebugValue

Do not add it to every custom Hook automatically.

Use it when the debugging information is genuinely useful.

Remember that it is primarily a developer experience tool, not a UI feature.

Common Mistakes With useSyncExternalStore

Do not use it simply because an application has global state.

If React state and Context solve the problem cleanly, they may be simpler.

useSyncExternalStore is particularly valuable for external stores and reusable state-management infrastructure.

Common Mistakes With useEffectEvent

Do not use Effect Events as a replacement for regular event handlers.

For example, a button click should normally remain:

<button onClick={handleClick}>
  Save
</button>

Effect Events solve a more specific problem involving Effects.

Common Mistakes With useActionState

Do not treat useActionState as simply another name for useState.

It is designed around Actions and their returned state.

Keep the Action responsible for the operation and the returned state responsible for communicating its result to the UI.

Common Mistakes With useOptimistic

Do not treat optimistic state as confirmed server state.

An optimistic UI is temporary until the actual operation succeeds.

Always consider what happens if:

  • The server rejects the operation.
  • Validation fails.
  • The network request fails.
  • Another user changes the same data.
  • The server returns a different result.

How These Hooks Fit Into React

The broader React Hooks ecosystem can be understood as several groups.

State Hooks

useState
useReducer

Used for managing component state.

Context

useContext

Used for reading shared Context values.

Ref Hooks

useRef
useImperativeHandle

Used for persistent mutable values and imperative interactions.

Effect Hooks

useEffect
useEffectEvent

Used for synchronization with external systems and separating reactive and non-reactive Effect logic.

Performance Hooks

useMemo
useCallback
useTransition
useDeferredValue

Used for memoization and responsive rendering.

Other Specialized Hooks

useId
useDebugValue
useSyncExternalStore
useActionState
useOptimistic

These solve more specialized problems.

A Web4Learners Example

Imagine Web4Learners has a tutorial platform.

Different Hooks could have different responsibilities.

useId
  ↓
Connect form labels and descriptions

useContext
  ↓
Share user/theme settings

useState
  ↓
Manage local UI state

useReducer
  ↓
Manage complex course state

useEffect
  ↓
Synchronize external systems

useSyncExternalStore
  ↓
Subscribe to an external store

useActionState
  ↓
Handle course enrollment Action

useOptimistic
  ↓
Show enrollment immediately

useTransition
  ↓
Keep expensive navigation responsive

This demonstrates an important React principle:

Different Hooks solve different problems. Good React code chooses the Hook that matches the problem instead of using one Hook for everything.

Hook Selection Guide

When building a component, start with the simplest solution.

Ask these questions:

Does the component need local changing data?

Use:

useState

Does the state transition involve many related actions?

Consider:

useReducer

Do many descendants need the same value?

Consider:

useContext

Do you need a DOM reference or persistent mutable value?

Consider:

useRef

Do you need to synchronize with an external system?

Consider:

useEffect

Is an expensive calculation repeated unnecessarily?

Consider:

useMemo

Does function identity matter for an optimization?

Consider:

useCallback

Does an update need to be deprioritized?

Consider:

useTransition

Can a consumer use a deferred value?

Consider:

useDeferredValue

Do you need a unique DOM ID?

Use:

useId

Do you need to subscribe to an external store?

Consider:

useSyncExternalStore

Do you need to manage an Action’s result and pending state?

Consider:

useActionState

Do you want to display an expected result immediately while an Action runs?

Consider:

useOptimistic

Hooks Should Solve Real Problems

It is tempting to use every Hook simply because it is available.

That is not necessary.

For example, this component:

function Greeting({ name }) {
  return <h1>Hello, {name}</h1>;
}

does not need:

useState
useEffect
useMemo
useCallback
useContext
useRef

It is already complete.

Good React development is not about using more Hooks.

It is about using the appropriate tools when they solve a real problem.

Practice Exercise

Create a small Web4Learners Enrollment Form that demonstrates several of the Hooks from this chapter.

The application should:

  • Generate accessible IDs with useId
  • Submit a form using useActionState
  • Display a pending state
  • Show an optimistic enrollment message using useOptimistic
  • Display validation errors
  • Show a success message after the Action completes

A simplified structure could look like this:

import {
  useActionState,
  useId,
  useOptimistic,
} from "react";

async function enrollAction(
  previousState,
  formData
) {
  const name = formData.get("name");

  if (!name) {
    return {
      error: "Name is required",
    };
  }

  return {
    success: `Welcome, ${name}!`,
  };
}

function EnrollmentForm() {
  const nameId = useId();

  const [
    state,
    formAction,
    isPending,
  ] = useActionState(
    enrollAction,
    {}
  );

  const [
    optimisticMessage,
    showOptimistic,
  ] = useOptimistic(
    "",
    (_, message) => message
  );

  function handleSubmit(formData) {
    const name = formData.get("name");

    showOptimistic(
      `Enrolling ${name}...`
    );

    return formAction(formData);
  }

  return (
    <form action={handleSubmit}>
      <label htmlFor={nameId}>
        Your Name
      </label>

      <input
        id={nameId}
        name="name"
        type="text"
      />

      <button disabled={isPending}>
        {isPending
          ? "Enrolling..."
          : "Enroll"}
      </button>

      {optimisticMessage && (
        <p>{optimisticMessage}</p>
      )}

      {state.error && (
        <p>{state.error}</p>
      )}

      {state.success && (
        <p>{state.success}</p>
      )}
    </form>
  );
}

The exact implementation of Actions and optimistic updates can vary depending on the React framework and application architecture, so treat this as a learning exercise rather than a complete production enrollment system.

Key Takeaways

  • React provides several specialized Hooks beyond the most commonly used Hooks.
  • useId generates unique IDs that are useful for connecting related DOM elements.
  • useId is particularly helpful for accessible forms.
  • useId should not be used as a list key or database identifier.
  • useDebugValue helps custom Hooks display useful information in React Developer Tools.
  • useDebugValue is primarily a developer and library-author tool.
  • useSyncExternalStore allows React to safely subscribe to external stores.
  • useSyncExternalStore is especially useful for external state-management libraries and reusable infrastructure.
  • useEffectEvent helps separate non-reactive logic from an Effect while allowing that logic to read current values.
  • Effect Events are not general-purpose replacements for event handlers.
  • useActionState manages the result and pending state of an Action.
  • useActionState is particularly useful for forms and Action-based workflows.
  • useOptimistic allows an application to show an expected result before an asynchronous operation finishes.
  • Optimistic state must eventually be reconciled with the actual server result.
  • Not every application needs these Hooks.
  • Choose Hooks based on the problem you are solving.
  • Prefer simpler React APIs when they are sufficient.
  • Specialized Hooks become particularly valuable when building complex applications, reusable components, libraries, forms, or integrations with external systems.

The most important idea to remember is:

React provides specialized Hooks for specialized problems. You do not need to use every Hook. Understand what each one solves, then choose the simplest Hook that fits your application’s needs.

Leave a Comment