Skip to content
Twilight React Playground
ثيم رائدaren

State and re-rendering

Beginner10 min

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.

like.js (plain DOM)
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
});
LikeButton.tsx
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.

Parentrenders:
clicks: 0
Childrenders:
likes: 0
memo(Child) with no propsrenders:
Skips the render when its parent renders, because its props did not change.
  • 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. memo lets a child skip that render when its props are equal.
  • The same value does nothing: React compares the old and new state with Object.is and 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.

HookSlot (subscribed)No handlers yet.
A plain component (not subscribed)
getHandlers().length read during render: 0

The 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
  • HookSlot calls hookRegistry.subscribe(name, …) in an effect and bumps a state counter when register or clear notifies it (src/hooks/HookSlot.tsx, src/hooks/HookRegistry.ts).
  • useWishlist keeps the ids in a module-level store read with useSyncExternalStore, fed by Salla.wishlist.event.onAdded and onRemoved; its server snapshot is []. useRouteId subscribes to the twilight context the same way.
  • useProduct holds the product in state and updates it from the SDK's product::price.updated event, 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.
Subscribing to data outside React
// 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, () => []);
}
Check yourself

Which of these does NOT make a component render again?