Skip to content

Detected patterns

Petar Liovic edited this page Sep 9, 2026 · 1 revision

Basis flags update patterns. A hit is not automatically a bug.

Each section is: what it saw, a small example, what the console usually prints, and a change that often helps. If the pattern is intentional, ignore it or add // @basis-ignore to that file.


1. Shared event, several roots

One trigger (click, timer, fetch) updates several state roots in the same window - often in different files or contexts.

That can be fine (login also flips theme). It can also mean one interaction owns state in too many places. The report names the event first so you look at the source, not every downstream write.

Often looks like

const handleLogin = () => {
  setUser(userData);       // AuthContext
  setTheme('dark');        // ThemeContext
  setLastLogin(Date.now()); // somewhere else
};

Change that sometimes helps

If those writes are really one transition, put them in one store or one reducer. If they are independent on purpose, leave them and ignore the line.

Console (health report)

🎯 REFACTOR PRIORITIES
1 ⚡ Global Event (handleLogin) (AuthContext.tsx)
   An external trigger is updating 3 roots.
   Impacts: user, theme, lastLogin

To see the edges: window.printBasisGraph() (see the README). Do not treat that dump as a second scoring system.


2. Double render (effect-driven update)

State A updates, an effect runs, state B updates on the next frame.

You paid for a paint, then immediately scheduled another. Sometimes that is required (subscribe, sync to an external store). Often B could be computed while rendering.

Often looks like

const [data, setData] = useState(null);
const [isValid, setIsValid] = useState(false);

useEffect(() => {
  setIsValid(check(data));
}, [data]);

Change that often helps

const [data, setData] = useState(null);
const isValid = check(data);

Console

⚡ BASIS | DOUBLE RENDER
📍 Location: ValidationForm.tsx
Issue: effect_L45 triggers isValid in a separate frame.
Fix: Derive isValid during render, or drop the effect.

3. Coupled local state

Two local variables keep updating in the same frames. They may be the same fact stored twice. They may just always change together.

Often looks like

const [firstName, setFirst] = useState('John');
const [fullName, setFull] = useState('John Doe');

Change that often helps

const [firstName, setFirst] = useState('John');
const fullName = `${firstName} Doe`;

Console

♊ BASIS | DUPLICATE STATE
📍 Location: UserProfile.tsx
Issue: firstName and fullName keep updating together.
Fix: Derive fullName during render if it is not its own source of truth.

4. Several booleans that move together

Same idea as above, with three or more flags. Easy to get impossible combinations (isLoading and isSuccess both true).

Often looks like

const [isLoading, setLoading] = useState(false);
const [isSuccess, setSuccess] = useState(false);
const [hasError, setError] = useState(false);

Change that often helps

const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');

Console

♊ BASIS | DUPLICATE STATE
Fix: Flags update together. A single status value is one way to make illegal combinations unrepresentable.

5. Local state that only copies Context

A local hook is updated whenever a context value updates, and does not have its own life.

Often looks like

const { user } = useContext(AuthContext);
const [localName, setLocalName] = useState(user.name);

useEffect(() => {
  setLocalName(user.name);
}, [user]);

Change that often helps

Read user.name in render. Delete the local copy unless you are drafting an unsaved edit.

Console

♊ BASIS | CONTEXT MIRRORING
Issue: localName keeps pace with AuthContext.
Fix: If the local value has no independent purpose, consume context directly.

6. Circuit breaker

A variable is updating far faster than a UI should (on the order of 150+ writes per second). Basis stops following that path so the tab stays alive.

Console

🛑 BASIS | CIRCUIT BREAKER
INFINITE LOOP
Variable: count
ACTION: further updates from this path are ignored by Basis.

This is a guard for the instrumented path, not a general React error boundary. Fix the effect that writes a dependency it also lists.


Live line vs health report

The live console line is one pair, as it happens.

printBasisReport() looks at the graph for the current window: sources, fan-out, several pairs at once.

If they disagree - e.g. the live line says “merge these two locals” and the report says “same click, two files” - read the report first, then decide. The report has more context. It is still not a verdict.

Clone this wiki locally