React
Subscribe React components to resolved GenomeJS traits with useGenomeTrait().
The React adapter connects a component to one resolved GenomeJS value.
const color = useGenomeTrait(genome, "color");When the Genome mutates, the component renders again with the latest value.
Installation
npm install @genomejs/core @genomejs/reactThe package requires React 18 or newer.
Exports
import { useGenomeTrait } from "@genomejs/react";Hook signature
useGenomeTrait(
genome: Genome,
name: string,
): PrimitiveParameters
| Parameter | Type | Description |
|---|---|---|
genome | Genome | Genome instance to observe |
name | string | Primitive or derived token name |
Return value
type Primitive = string | number;The hook returns the current resolved value of the requested trait.
Create a shared Genome
Create lib/theme-genome.ts:
import { Genome } from "@genomejs/core";
export const themeGenome = new Genome({
primitives: {
lightSurface: "#ffffff",
darkSurface: "#121620",
lightForeground: "#171923",
darkForeground: "#f7f8fb",
baseSpacing: 16,
},
tokens: {
surface: (dna, context) =>
context.mode === "dark" ? dna.darkSurface : dna.lightSurface,
foreground: (dna, context) =>
context.mode === "dark" ? dna.darkForeground : dna.lightForeground,
spacing: (dna, context) => {
const scale = typeof context.scale === "number" ? context.scale : 1;
return `${Number(dna.baseSpacing) * scale}px`;
},
},
});
themeGenome.mutate({
mode: "light",
scale: 1,
});A shared module-level instance is usually preferable to constructing a new Genome every time a component renders.
Subscribe from a component
Create components/theme-preview.tsx:
"use client";
import { useGenomeTrait } from "@genomejs/react";
import { themeGenome } from "@/lib/theme-genome";
export function ThemePreview() {
const surface = useGenomeTrait(themeGenome, "surface");
const foreground = useGenomeTrait(themeGenome, "foreground");
const spacing = useGenomeTrait(themeGenome, "spacing");
return (
<section
style={{
display: "grid",
gap: String(spacing),
padding: String(spacing),
backgroundColor: String(surface),
color: String(foreground),
}}
>
<h2>Reactive theme</h2>
<p>Current spacing: {spacing}</p>
</section>
);
}Each hook call subscribes the component to one token name.
Mutate from React
"use client";
import { useGenomeTrait } from "@genomejs/react";
import { themeGenome } from "@/lib/theme-genome";
export function ThemeControl() {
const surface = useGenomeTrait(themeGenome, "surface");
function useDarkMode() {
themeGenome.mutate({
mode: "dark",
});
}
function useLightMode() {
themeGenome.mutate({
mode: "light",
});
}
return (
<div>
<p>Current surface: {surface}</p>
<button type="button" onClick={useLightMode}>
Light
</button>
<button type="button" onClick={useDarkMode}>
Dark
</button>
</div>
);
}Calling mutate() resolves the Genome again. Components subscribed to traits then receive the latest snapshots.
How the hook works
The adapter connects GenomeJS to React using:
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);Conceptually:
Component renders
↓
useGenomeTrait reads getTrait(name)
↓
React subscribes to genome.subscribe()
↓
genome.mutate() resolves
↓
Genome notifies subscribers
↓
React reads the new snapshot
↓
Component renders againThe adapter uses the same Genome snapshot function for browser and server rendering.
Next.js
@genomejs/react is a client-marked module, so components calling useGenomeTrait() should be Client Components.
"use client";
import { useGenomeTrait } from "@genomejs/react";The shared Genome itself may be defined in a separate module:
import { Genome } from "@genomejs/core";
export const genome = new Genome({
primitives: {},
tokens: {},
});The Core constructor is safe when no browser DOM exists. Its default target is null outside the browser.
Server rendering
The React adapter supports reading the current trait during React server rendering.
function Message({ genome }: { genome: Genome }) {
const message = useGenomeTrait(genome, "message");
return <p>{message}</p>;
}For hydration to remain consistent, use the same initial Genome context for the server output and the browser’s first render.
Apply browser-only context changes after mounting when they depend on APIs such as window.matchMedia().
"use client";
import { useEffect } from "react";
export function BrowserModeBinding() {
useEffect(() => {
const query = window.matchMedia("(prefers-color-scheme: dark)");
function updateMode() {
themeGenome.mutate({
mode: query.matches ? "dark" : "light",
});
}
updateMode();
query.addEventListener("change", updateMode);
return () => {
query.removeEventListener("change", updateMode);
};
}, []);
return null;
}Keep the Genome instance stable
Avoid constructing the Genome directly during every render:
function Preview() {
const genome = new Genome({
primitives: {},
tokens: {},
});
const value = useGenomeTrait(genome, "value");
return <p>{value}</p>;
}Each render creates another instance, which changes the subscription source.
Prefer a module-level instance:
export const genome = new Genome(config);Or create it once with React state:
const [genome] = useState(() => new Genome(config));Reading multiple traits
Call the hook once for each value:
const surface = useGenomeTrait(genome, "surface");
const foreground = useGenomeTrait(genome, "foreground");The current adapter does not provide a multi-trait selector hook.
CSS custom properties
A component does not need to read every trait through React.
GenomeJS also expresses values as CSS properties:
.card {
color: var(--g-foreground);
background: var(--g-surface);
gap: var(--g-spacing);
}Use useGenomeTrait() when component logic or rendered text needs the JavaScript value.
Use CSS variables when styling can remain in CSS.
Unknown trait behavior
The hook reads its snapshot through genome.getTrait(name).
An unknown name therefore throws:
useGenomeTrait(genome, "missingTrait");Current error:
Unknown token: "missingTrait"Check spelling and ensure the primitive or token exists in the Genome configuration.
Manual subscriptions
Use the Core subscribe() method for imperative integrations:
useEffect(() => {
return genome.subscribe(() => {
synchronizeExternalLibrary(genome.getTrait("spacing"));
});
}, []);For values rendered by React, prefer useGenomeTrait().
Common problems
The component does not update
Confirm that:
- The component calls
useGenomeTrait(). - The same Genome instance is mutated.
- The requested token name is correct.
- The component has not created a separate Genome instance.
Invalid hook call
In a monorepo, ensure the application and @genomejs/react resolve the same React installation.
Hook used in a Server Component
Add the Client Component directive:
"use client";A style value has the wrong TypeScript type
The hook returns string | number.
Convert values where a specific string is expected:
style={{
color: String(color),
}}Notes
- The hook subscribes through
useSyncExternalStore. - It re-renders when the Genome notifies subscribers.
- Its return type is
string | number. - The Genome instance should remain stable.
- Unknown trait names throw.
- Manual cleanup is handled by React’s external-store lifecycle.
- React 18 or newer is required.