The code
One store, created once. Reset writes the initial profile back with useProfile.setState(initialProfile()).
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:
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:
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
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>
);
}