-
Notifications
You must be signed in to change notification settings - Fork 3
Detected patterns
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.
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.
const handleLogin = () => {
setUser(userData); // AuthContext
setTheme('dark'); // ThemeContext
setLastLogin(Date.now()); // somewhere else
};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.
🎯 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.
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.
const [data, setData] = useState(null);
const [isValid, setIsValid] = useState(false);
useEffect(() => {
setIsValid(check(data));
}, [data]);const [data, setData] = useState(null);
const isValid = check(data);⚡ BASIS | DOUBLE RENDER
📍 Location: ValidationForm.tsx
Issue: effect_L45 triggers isValid in a separate frame.
Fix: Derive isValid during render, or drop the effect.
Two local variables keep updating in the same frames. They may be the same fact stored twice. They may just always change together.
const [firstName, setFirst] = useState('John');
const [fullName, setFull] = useState('John Doe');const [firstName, setFirst] = useState('John');
const fullName = `${firstName} Doe`;♊ 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.
Same idea as above, with three or more flags. Easy to get impossible combinations (isLoading and isSuccess both true).
const [isLoading, setLoading] = useState(false);
const [isSuccess, setSuccess] = useState(false);
const [hasError, setError] = useState(false);const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');♊ BASIS | DUPLICATE STATE
Fix: Flags update together. A single status value is one way to make illegal combinations unrepresentable.
A local hook is updated whenever a context value updates, and does not have its own life.
const { user } = useContext(AuthContext);
const [localName, setLocalName] = useState(user.name);
useEffect(() => {
setLocalName(user.name);
}, [user]);Read user.name in render. Delete the local copy unless you are drafting an unsaved edit.
♊ BASIS | CONTEXT MIRRORING
Issue: localName keeps pace with AuthContext.
Fix: If the local value has no independent purpose, consume context directly.
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.
🛑 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.
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.