Updating Document Title in React Without Helmet

react-helmet has long been the default for managing the head in React, but it is an extra dependency with its own quirks — and modern React often does not need it. For client-rendered routes you can manage the title with a small hook over the native document API, and on React 19 you can render <title> and <meta> directly in JSX and let React hoist them. This keeps titles correct per route as part of programmatic title and meta tag updates.

Two dependency-free ways to set the head in React A hook that writes document.title in an effect, and React 19 rendering title and meta in JSX, both landing in the document head without react-helmet. No react-helmet — two paths to a correct head useDocumentTitle hook React 16–18 document.title = title effect on [title], restore prev <title> in JSX React 19 React hoists it works during SSR <head> correct title per route
Both routes to the head skip react-helmet: the effect-based hook for older React, native hoisting on React 19.

Step-by-step fix

  1. Write a minimal title hook. Set document.title in an effect keyed to the route’s title.

    // useDocumentTitle.js
    import { useEffect } from 'react';
    export function useDocumentTitle(title) {
      useEffect(() => {
        const prev = document.title;
        document.title = title;
        return () => { document.title = prev; }; // restore on unmount
      }, [title]);
    }
  2. Manage meta tags by mutation, not appending. Reuse one tag per name so navigation does not pile up duplicates.

    // ✅ Update an existing meta tag in place
    function setMeta(name, content) {
      let tag = document.querySelector(`meta[name="${name}"]`);
      if (!tag) { tag = document.createElement('meta'); tag.name = name; document.head.appendChild(tag); }
      tag.content = content;
    }
  3. Prefer native metadata hoisting on React 19. Rendering metadata in JSX removes the need for any library and works during SSR.

    // ✅ React 19 hoists these into <head> automatically
    function ProductHead({ product }) {
      return (
        <>
          <title>{product.name}</title>
          <meta name="description" content={product.summary} />
        </>
      );
    }
  4. Set the title as early as possible for client-rendered routes so the render snapshot captures the correct value.

The title hook saves and restores the previous title Mounting Home sets the title, entering Product saves Home as the previous value and sets Product, and unmounting Product restores Home from the saved value. Save-and-restore keeps the title correct on back navigation Mount Home title: Home prev saved: — Enter Product save prev = Home title: Product Unmount Product restore prev title: Home ✓ The effect cleanup returns the title the parent route had
Because the effect stashes the previous title and restores it on cleanup, leaving a route returns the title its parent had set.

Validation

  • document.title matches the route on every navigation.
  • No duplicate meta tags accumulate as you navigate.
  • GSC URL Inspection rendered HTML shows the right title and description.
  • React 19 path: <title> appears in <head> in the SSR output, not buried in the body.
Where each approach lands in the SSR snapshot The effect hook path leaves a default title in the SSR HTML until JavaScript runs, while rendering the title in JSX on React 19 puts the correct title in the head of the SSR output. What the crawler sees in the initial SSR HTML Effect hook (React 16–18) <head> <title>App</title> default until JS runs ✗ <title> in JSX (React 19) <head> <title>Product name</title> correct in SSR output ✓
The effect-based hook only corrects the title after hydration; React 19's hoisting puts the right title in the head before any script runs.

Reference

// Dependency-free head management for a route component
import { useDocumentTitle } from './useDocumentTitle';

export function ProductPage({ product }) {
  useDocumentTitle(`${product.name} — Example`); // SEO: per-route title
  useEffect(() => {
    setMeta('description', product.summary);       // mutate, never append
  }, [product]);
  return <ProductView product={product} />;
}
One route component drives both head updates without a library The route component calls useDocumentTitle to set document.title and setMeta inside an effect to mutate the description tag in place, and both land in the document head as the correct per-route metadata. One component, two hooks, no react-helmet ProductPage route component useDocumentTitle(title) → document.title setMeta() in effect → mutate description tag <head> correct title + meta per route
The route component wires both dependency-free hooks — a title setter and an in-place meta mutation — into a single correct head per route.

Frequently Asked Questions

Do I still need react-helmet in modern React? Not necessarily. React 19 hoists title and meta elements rendered anywhere in the tree into the document head, covering most needs natively. For earlier versions, a small custom hook that sets document.title and mutates meta tags is enough for many apps without the extra dependency.

Will a client-only title update be seen by Google? Google can pick up a client-set title after it renders the page, but it is more reliable when the title is in the server or prerendered HTML. For client-rendered routes, set the title as early as possible and ensure it is correct in the render snapshot.

← Back to Programmatic Title & Meta Tag Updates