State and re-rendering
Why React runs your component again, how to watch it happen, and how engine hooks follow data that changes outside React.
Your component is a function, and React calls it whenever the screen might need to change. Each call is a render; a later call is a re-render. After a render React compares the new JSX with the previous one and touches only the parts of the page that differ.
State is a value React keeps for a component between renders. You change it with the setter from useState, and that call is what asks React to render again.
let likes = 0;
const button = document.querySelector('#like');
button.addEventListener('click', () => {
likes += 1; // 1. change the data
button.textContent = '♥ ' + likes; // 2. remember to update the page yourself
});
import { useState } from 'react';
export function LikeButton() {
const [likes, setLikes] = useState(0);
// Each click stores the new number AND asks React to call LikeButton again.
return <button onClick={() => setLikes(likes + 1)}>♥ {likes}</button>;
}
Watch renders happen
Each box counts its own renders. Try the three buttons and predict each counter first.
- A state change re-renders the component that owns the state, and every component inside it.
- A parent's render re-runs its children, even when nothing they receive changed.
memolets a child skip that render when its props are equal. - The same value does nothing: React compares the old and new state with
Object.isand bails out. - A changed plain variable never re-renders anything. React cannot see it.
Data that changes outside React
Much of a store's data does not live in React state: the wishlist is kept by the Salla SDK, hook slot content lives in the engine's registry. For a component to follow such data it must subscribe: ask to be told when the data changes, and re-render then.
The left panel is the engine's HookSlot, which subscribes to the hook registry. The right panel reads the same registry during render without subscribing. The buttons change the registry with plain function calls and never touch React state.
getHandlers().length read during render: 0The slot updates at once; the plain reader shows an old number until something else makes it render. That is the whole difference between reading data and subscribing to it.
In engine terms
HookSlotcallshookRegistry.subscribe(name, …)in an effect and bumps a state counter whenregisterorclearnotifies it (src/hooks/HookSlot.tsx, src/hooks/HookRegistry.ts).useWishlistkeeps the ids in a module-level store read withuseSyncExternalStore, fed bySalla.wishlist.event.onAddedandonRemoved; its server snapshot is[].useRouteIdsubscribes to the twilight context the same way.useProductholds the product in state and updates it from the SDK'sproduct::price.updatedevent, so a price changes when a shopper picks an option.- A setter does not change the variable in the current render:
setLikes(likes + 1)twice in one click adds one. Use the updater form,setLikes((n) => n + 1), when the next value depends on the previous one. - In development React renders components twice and mounts effects twice (
StrictMode) to expose impure code. The counters above ignore that second pass.
// The pattern useWishlist uses (src/hooks/useWishlist.ts), shortened.
import { useSyncExternalStore } from 'react';
import { getSallaSDK } from '@salla.sa/twilight-theme-engine/utils';
let ids: number[] = [];
let listening = false;
const listeners = new Set<() => void>();
function subscribe(listener: () => void) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
function listenToSalla() {
if (listening || typeof window === 'undefined') return; // there is no SDK on the server
listening = true;
// Salla's SDK announces a change; the store updates and tells every subscriber.
getSallaSDK()?.wishlist.event.onAdded((_response: unknown, id: number) => {
ids = [...ids, id]; // a new array, so React sees a change
listeners.forEach((listener) => listener());
});
}
export function useWishlistIds() {
listenToSalla();
return useSyncExternalStore(subscribe, () => ids, () => []);
}
Reference: HookSlot, hookRegistry and useWishlist.