mirror of
https://github.com/duthaho/claudekit.git
synced 2026-06-10 20:24:57 +03:00
6.0 KiB
6.0 KiB
React Patterns Quick Reference
Hook Rules
- Only call hooks at the top level (not inside loops, conditions, or nested functions)
- Only call hooks from React function components or custom hooks
- Custom hooks must start with
use - Hook call order must be identical on every render
// BAD
if (isLoggedIn) {
const [user, setUser] = useState(null); // Conditional hook call
}
// GOOD
const [user, setUser] = useState(null);
// Use the state conditionally instead
Custom Hook Recipes
useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
// Usage
const [theme, setTheme] = useLocalStorage("theme", "dark");
useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Usage
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 300);
useEffect(() => {
fetchResults(debouncedSearch);
}, [debouncedSearch]);
useMediaQuery
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
);
useEffect(() => {
const mql = window.matchMedia(query);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, [query]);
return matches;
}
// Usage
const isMobile = useMediaQuery("(max-width: 768px)");
usePrevious
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
});
return ref.current;
}
// Usage
const prevCount = usePrevious(count);
Compound Component Pattern
// Components that work together sharing implicit state
const Accordion = ({ children }: { children: React.ReactNode }) => {
const [openIndex, setOpenIndex] = useState<number | null>(null);
return (
<AccordionContext.Provider value={{ openIndex, setOpenIndex }}>
<div role="tablist">{children}</div>
</AccordionContext.Provider>
);
};
const AccordionItem = ({ index, title, children }: {
index: number; title: string; children: React.ReactNode;
}) => {
const { openIndex, setOpenIndex } = useContext(AccordionContext);
const isOpen = openIndex === index;
return (
<div>
<button onClick={() => setOpenIndex(isOpen ? null : index)}>
{title}
</button>
{isOpen && <div>{children}</div>}
</div>
);
};
Accordion.Item = AccordionItem;
// Usage - clean, declarative API
<Accordion>
<Accordion.Item index={0} title="Section 1">Content 1</Accordion.Item>
<Accordion.Item index={1} title="Section 2">Content 2</Accordion.Item>
</Accordion>
Context Patterns
Split State and Dispatch
// Prevent unnecessary re-renders by splitting contexts
const StateContext = createContext<State | null>(null);
const DispatchContext = createContext<Dispatch | null>(null);
function Provider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>
{children}
</StateContext.Provider>
</DispatchContext.Provider>
);
}
// Components that only dispatch won't re-render on state changes
function useAppDispatch() {
const ctx = useContext(DispatchContext);
if (!ctx) throw new Error("Must be used within Provider");
return ctx;
}
Performance Checklist
When to Memoize
| Situation | Solution |
|---|---|
| Expensive computation | useMemo(() => compute(data), [data]) |
| Stable callback for child | useCallback(fn, [deps]) |
| Prevent child re-render | React.memo(Component) |
| Stable object/array prop | useMemo(() => ({ ... }), [deps]) |
When NOT to Memoize
- Simple/cheap calculations
- Primitives (strings, numbers, booleans) -- already stable
- Functions only used in the same component
- Components that are cheap to render
Key Optimizations
// 1. Move state down (colocate state with where it's used)
// BAD: parent re-renders everything
function Parent() {
const [hover, setHover] = useState(false);
return <><ExpensiveChild /><HoverTarget onHover={setHover} /></>;
}
// GOOD: isolate the stateful part
function Parent() {
return <><ExpensiveChild /><HoverSection /></>;
}
function HoverSection() {
const [hover, setHover] = useState(false);
return <HoverTarget onHover={setHover} />;
}
// 2. Pass children to avoid re-rendering them
function ScrollTracker({ children }: { children: React.ReactNode }) {
const [scroll, setScroll] = useState(0);
// children are created by parent, not re-created on scroll change
return <div onScroll={e => setScroll(e.currentTarget.scrollTop)}>{children}</div>;
}
// 3. Use key to reset component state
<Form key={selectedId} initialData={data} />
// 4. Lazy load heavy components
const HeavyChart = lazy(() => import("./HeavyChart"));
<Suspense fallback={<Skeleton />}>
<HeavyChart data={data} />
</Suspense>
React DevTools Profiler Checklist
- Enable "Highlight updates" to spot unnecessary re-renders
- Record a profiler session and look for:
- Components re-rendering without visible changes
- Long render times (>16ms blocks frames)
- Cascading re-renders from context changes
- Fix with: state colocation, memo, context splitting, or external state