rEFui Best Practices & Troubleshooting
This guide consolidates performance tips, reactive patterns, and common pitfalls. Prefer these patterns before reaching for custom memoization or heavy abstractions.
General Architecture
Mental model: retained rendering + signals
- No VDOM diff; signals drive precise updates.
- Use signals only when values must stay reactive; literals are fine for static props/children.
Renderer Instance Management
Create renderer instances once at your application's entry point (typically main.js or index.js). Avoid creating multiple renderer instances within components.
// ✅ Good: Create renderer once in main.js
import { createDOMRenderer } from 'refui/dom';
import { defaults } from 'refui/browser';
// Single renderer instance
export const renderer = createDOMRenderer(defaults);
// Use throughout your app
renderer.render(document.getElementById('app'), App);
// ❌ Avoid: Creating renderers in components
const MyComponent = () => {
// Don't do this - creates unnecessary renderer instances
const renderer = createDOMRenderer(defaults);
return <div>Hello</div>;
};
Component Organization
Keep your components focused and reusable. When components grow large, consider breaking them into smaller, composable pieces.
// ✅ Good: Small, focused components
const UserName = ({ user }) => <span>{user.name}</span>;
const UserEmail = ({ user }) => <span>{user.email}</span>;
const UserCard = ({ user }) => (
<div>
<UserName user={user} />
<UserEmail user={user} />
</div>
);
Reactivity Patterns
Reactivity & object properties
Signals track reads/writes on the signal itself, not nested object mutations.
// ❌ Mutation won’t notify
tracks.value[0].sampleRate = 44100
// ✅ Preferred: mutate in place + trigger for GC-friendly updates
tracks.value[0].sampleRate = 44100
tracks.trigger() // re-run dependents without recreating the array
// ✅ Replace object
const next = [...tracks.value]
next[0] = { ...next[0], sampleRate: 44100 }
tracks.value = next
// ✅ Nested signals for frequent updates
track.sampleRate.value = 44100
Computed dependency tracking through branches
Dependencies are discovered when control flow reaches their reads.
const info = computed(() => {
const t = track.value
if (!t) return 'Ready'
return `${t.title} - ${metadata.value}`
})
Here metadata starts being observed when track becomes truthy. Touch it before the branch only when changes to metadata must rerun the computation while track is still falsy.
derivedExtract with nullable sources
derivedExtract tolerates null/undefined sources. You can extract directly without a safe wrapper:
const current = signal<Track | null>(null)
const { title, sampleRate } = derivedExtract(current, 'title', 'sampleRate')
watch vs useEffect vs onDispose
watch: reactive computations, no external cleanup.useEffect: setup + cleanup that reruns when deps change.onDispose: teardown only.
Performance Optimization
Keeping updates local
- Lists: Use
onConditionor class toggles for per-row state instead of widecomputedfan-out. - Signals: Keep state localized; split big blobs into per-branch signals.
- Extracting: Use
extract/derivedExtractso subtrees only subscribe to the fields they read.
Coalescing updates
Use createDefer / createSchedule with cancelable deferrers to coalesce expensive work during rapid input (e.g. search boxes or window resizing).
Component Design
Keeping JSX lean
Define complex computeds at the top of a component; keep the JSX return as a simple "view" of your reactive data.
const fileInfo = computed(() => /* ... */)
return <div>{fileInfo}</div>
Dependency Scope
Use onCondition to scope fan-out in large lists or complex conditional branches. This prevents every item in a list from re-evaluating when a single global flag changes.