Skip to content
react-global-state-hooks
Interactive example · Subscriptions

Shared state, precise updates

Change a field. See which components render—and which stay untouched. Four components share one store.

Try it hereLive example
Application preview

Loading the example…

store.ts
import { createGlobalState } from 'react-global-state-hooks';

export type Profile = {
  name: string;
  role: string;
  clicks: number;
};

export const initialProfile = (): Profile => ({
  name: 'Ada',
  role: 'Engineer',
  clicks: 0,
});

export const useProfile = createGlobalState(initialProfile, {
  name: 'profile',
});
Ready. Make a change to begin.Reset returns this example to its initial state.
Take it with you

A selection is a dependency, not a copy of the store.

01

Type in Name. The Name card and the whole-state card render again; Role and Clicks keep their counts.

02

The whole-state card has no selector, so it renders for every change. That is the cost every component would pay if it subscribed to the whole store.

03

Setting the state to the same object again renders nothing. The counters count committed renders, not selected-value changes.

Read the full guide

The code

One store, created once. Reset writes the initial profile back with useProfile.setState(initialProfile()).

store.ts
import { createGlobalState } from 'react-global-state-hooks';

export type Profile = {
  name: string;
  role: string;
  clicks: number;
};

export const initialProfile = (): Profile => ({
  name: 'Ada',
  role: 'Engineer',
  clicks: 0,
});

export const useProfile = createGlobalState(initialProfile, {
  name: 'profile',
});

A component subscribes to just the field it shows:

NameCard.tsx
import { RenderCount } from '../shared/RenderCount';
import { useProfile } from './store';

export function NameCard() {
  // Subscribes to `name` only: role and click changes never re-render this component.
  const [name, setProfile] = useProfile((profile) => profile.name);

  return (
    <section className="demo-card" aria-label="Name card">
      <RenderCount />
      <label>
        Name
        <input
          value={name}
          maxLength={40}
          onChange={(event) =>
            setProfile((profile) => ({
              ...profile,
              name: event.target.value,
            }))
          }
        />
      </label>
    </section>
  );
}

The component without a selector:

WholeStateCard.tsx
import { RenderCount } from '../shared/RenderCount';
import { useProfile } from './store';

export function WholeStateCard() {
  // No selector: subscribes to the whole state, so every change re-renders this component.
  const [profile] = useProfile();

  return (
    <section className="demo-card demo-card--wide" aria-label="Whole state card">
      <RenderCount />
      <span>Whole state</span>
      <pre className="demo-json">{JSON.stringify(profile, null, 2)}</pre>
      <p className="demo-caption">No selector: this card subscribes to the whole store and renders for every change.</p>
    </section>
  );
}

How the counters work

RenderCount.tsx
import { useEffect, useRef } from 'react';

/**
 * Shows how many times the component that renders it has committed.
 *
 * Counting happens in effects, not during render, so React Strict Mode's double render in
 * development does not inflate the number. The first commit is the initial "1"; the cleanup resets
 * the counter so Strict Mode's simulated unmount/remount does not count as an update either.
 * The element is updated directly, so counting never causes a render of its own.
 */
export function RenderCount() {
  const element = useRef<HTMLSpanElement>(null);
  const count = useRef(1);
  const isFirstCommit = useRef(true);

  useEffect(
    () => () => {
      count.current = 1;
      isFirstCommit.current = true;
    },
    [],
  );

  // No dependency array on purpose: run after every commit of the parent component.
  useEffect(() => {
    if (isFirstCommit.current) {
      isFirstCommit.current = false;
      return;
    }

    count.current += 1;
    const node = element.current;
    if (!node) return;

    node.textContent = String(count.current);
    const chip = node.parentElement;
    chip?.classList.remove('render-count--flash');
    void node.offsetWidth; // restart the CSS animation
    chip?.classList.add('render-count--flash');
  });

  return (
    <span className="render-count" title="Times this component has rendered">
      <span ref={element} data-testid="render-count">
        1
      </span>
      renders
    </span>
  );
}

Related documentation

Keep exploring

Another piece of the model.

↑↓ Navigate ↵ Open Esc CloseRGSH docs
Get started

Shared state. Precise subscriptions.
Built-in DevTools.