React JS Interviw Question: The codeing challenge and machine round
100 Comprehensive React JS Interview Questions & Answers. Master everything from Beginner hooks to Expert-level React JS coding challenge and machine round interview.
Beginner Level
React is an open-source JavaScript library created by Facebook for building user interfaces, particularly single-page applications. Its core features are:
- Component-based architecture — UI is split into reusable, self-contained pieces.
- Virtual DOM — React keeps a lightweight in-memory copy of the real DOM and only updates what changed, boosting performance.
- JSX — A syntax extension that lets you write HTML-like code inside JavaScript.
- Unidirectional data flow — Data flows from parent to child via props, making apps easier to debug.
- Hooks — Functions like
useStateanduseEffectthat add state and lifecycle behavior to functional components.
JSX (JavaScript XML) is a syntax extension that allows you to write HTML-like markup inside JavaScript files. It is not valid JavaScript — Babel transpiles it into React.createElement() calls at build time.
// JSX
const element = <h1 className="title">Hello, World!</h1>;
// What Babel compiles it to
const element = React.createElement('h1', { className: 'title' }, 'Hello, World!');JSX makes component code more readable and easier to reason about compared to chained createElement calls.
// Class Component
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// Functional Component (preferred)
function Greeting({ name }) {
return <h1>Hello, {name}</h1>;
}Functional components are simpler, use less boilerplate, and since React 16.8 can use Hooks for state and side-effects. Class components require this, lifecycle methods, and are generally more verbose. New code should prefer functional components.
Props (short for properties) are read-only inputs passed from a parent component to a child component. They make components reusable by letting the parent control child behavior/appearance.
function Button({ label, color }) {
return <button style={{ background: color }}>{label}</button>;
}
// Usage
<Button label="Submit" color="blue" />Props are immutable inside the child — the child must never modify them directly.
State is mutable data managed inside a component. When state changes, the component re-renders. Props are immutable data passed from outside.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}Think of props as arguments to a function and state as local variables that persist across renders.
The Virtual DOM is a lightweight JavaScript object tree that mirrors the real DOM. When state or props change, React:
- Renders a new Virtual DOM tree.
- Diffs it against the previous tree (reconciliation).
- Computes the minimal set of real DOM mutations.
- Applies only those changes to the actual browser DOM.
This batched, minimal-update strategy is far faster than naive full-page re-renders.
useState work? Give an example.useState is a Hook that adds local state to a functional component. It returns a tuple: the current value and a setter function.
import { useState } from 'react';
function Toggle() {
const [isOn, setIsOn] = useState(false);
return (
<button onClick={() => setIsOn(prev => !prev)}>
{isOn ? 'ON' : 'OFF'}
</button>
);
}The functional updater form prev => !prev is preferred when the new value depends on the old one, because React may batch state updates.
useEffect and when do you use it?useEffect lets you perform side-effects (data fetching, subscriptions, DOM mutations, timers) after render. It runs after the component renders and optionally cleans up before re-running.
import { useState, useEffect } from 'react';
function UserCard({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser);
// cleanup (runs before next effect or unmount)
return () => setUser(null);
}, [userId]); // re-runs only when userId changes
if (!user) return <p>Loading...</p>;
return <p>{user.name}</p>;
}Hooks are functions that let functional components tap into React features that were previously only available in class components.
useState— local component stateuseEffect— side-effects and lifecycleuseContext— consume a React contextuseRef— mutable ref object / DOM accessuseMemo— memoize expensive computed valuesuseCallback— memoize callback functionsuseReducer— complex state with reducer pattern
Rules of Hooks: only call at the top level, only call inside React functions — never inside conditionals or loops.
key prop in lists?The key prop helps React identify which items in a list have changed, been added, or removed during reconciliation. Keys must be stable, unique among siblings, and ideally come from your data (e.g., database IDs).
const items = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
function List() {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li> // ✅ stable, unique
))}
</ul>
);
}Avoid using array index as key when the list can be reordered — this causes subtle re-render bugs.
Conditional rendering lets you show or hide UI based on state or props. Common patterns:
function Alert({ isError, message }) {
// if/else
if (isError) return <div className="error">{message}</div>;
// ternary
return <div>{message ? message : 'No messages'}</div>;
// short-circuit &&
return <div>{message && <span>{message}</span>}</div>;
}React events use camelCase names and receive a SyntheticEvent — a cross-browser wrapper around the native event.
function Form() {
function handleSubmit(e) {
e.preventDefault(); // prevent page reload
console.log('submitted');
}
return (
<form onSubmit={handleSubmit}>
<input onChange={e => console.log(e.target.value)} />
<button type="submit">Send</button>
</form>
);
}React.Fragment and why is it useful?Fragments let you group multiple elements without adding an extra DOM node. This avoids invalid HTML (e.g., a <tr> inside a <div>) and keeps the DOM clean.
// Short syntax
function Columns() {
return (
<>
<td>Name</td>
<td>Age</td>
</>
);
}
// With key (must use long form)
items.map(item => (
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.def}</dd>
</React.Fragment>
))useRef? Give two use cases.useRef returns a mutable object { current: value } that persists across renders without causing re-renders when changed.
// Use case 1: access a DOM node
function FocusInput() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus</button>
</>
);
}
// Use case 2: store a mutable value (e.g. previous state)
function Timer() {
const timerIdRef = useRef(null);
const start = () => { timerIdRef.current = setInterval(tick, 1000); };
const stop = () => clearInterval(timerIdRef.current);
// ...
}Prop drilling happens when you must pass data through many intermediate components just to reach a deeply nested consumer that actually needs it.
// theme has to travel A → B → C even though B doesn't use it
<A theme="dark" />
<B theme={theme} /> // B just passes it down
<C theme={theme} /> // C actually uses itProblems: tight coupling, verbose code, hard to refactor. Solutions include React Context, Redux, Zustand, or component composition.
Context provides a way to share values (theme, auth, locale) across the component tree without prop drilling.
const ThemeContext = React.createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div className={theme}>Toolbar</div>;
}Context should be used for low-frequency global data. For high-frequency updates, prefer a state manager to avoid unnecessary re-renders.
When two sibling components need to share state, you lift the state to their closest common ancestor and pass it down as props.
function Parent() {
const [value, setValue] = useState('');
return (
<>
<Input value={value} onChange={setValue} />
<Display value={value} />
</>
);
}
function Input({ value, onChange }) {
return <input value={value} onChange={e => onChange(e.target.value)} />;
}
function Display({ value }) {
return <p>{value}</p>;
}A controlled component has its form input value driven by React state. A uncontrolled component manages its own state internally via the DOM; you read the value with a ref.
// Controlled
const [text, setText] = useState('');
<input value={text} onChange={e => setText(e.target.value)} />
// Uncontrolled
const inputRef = useRef();
<input ref={inputRef} defaultValue="hello" />
// read: inputRef.current.valueControlled components give you full control over validation and transformations on every keystroke. Uncontrolled components are simpler for basic forms and integrating with non-React code.
React.StrictMode?StrictMode is a developer tool that helps you spot potential problems. It intentionally double-invokes render functions, state initializers, and effects (in development) to surface side-effects written incorrectly. It has no effect in production builds.
<React.StrictMode>
<App />
</React.StrictMode>Warnings it catches: deprecated API usage, impure render side-effects, unexpected re-render issues, and missing cleanup in effects.
State must be treated as immutable. Always return a new object/array instead of mutating the existing one.
// ❌ Wrong — mutates directly
state.user.name = 'Alice'; setState(state);
// ✅ Correct — spread into new object
setState(prev => ({ ...prev, user: { ...prev.user, name: 'Alice' } }));
// ✅ Array: add item
setItems(prev => [...prev, newItem]);
// ✅ Array: remove item
setItems(prev => prev.filter(item => item.id !== targetId));
// ✅ Array: update item
setItems(prev => prev.map(item => item.id === targetId ? { ...item, done: true } : item));null and undefined rendering in JSX?Both null, undefined, and false render nothing — they are valid children that produce no DOM output. This makes them ideal for conditional rendering.
function Component({ show }) {
return (
<div>
{show && <p>Visible!</p>} // nothing rendered when show=false
{null} // nothing rendered
{0} // ⚠️ renders "0"! be careful
</div>
);
}Note: the number 0 does render — a common footgun when using count && <Comp />.
// Modern: destructuring defaults
function Button({ label = 'Click me', color = 'blue' }) {
return <button style={{ color }}>{label}</button>;
}
// Legacy: static property
Button.defaultProps = { label: 'Click me', color: 'blue' };Destructuring defaults are preferred in modern React since defaultProps may be removed in a future major version.
children prop and how is it used?The children prop contains everything placed between the component’s opening and closing tags, enabling composable wrapper components.
function Card({ children, title }) {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
}
// Usage
<Card title="Hello">
<p>I am a child!</p>
</Card>In React, the style attribute accepts a JavaScript object with camelCased property names and string values (not a CSS string).
const styles = {
backgroundColor: '#0d1117',
fontSize: '16px',
marginTop: 8, // numbers default to px
fontWeight: 'bold',
};
<div style={styles}>Styled</div>
// or inline:
<div style={{ color: 'red' }}>Red</div>setState multiple times in a row?React batches multiple state updates in event handlers (and in React 18+, everywhere including async code) into a single re-render for performance.
function Component() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
function handleClick() {
setA(1); // batched
setB(2); // batched
// → only ONE re-render occurs
}
}If you need the current state value based on the previous update, use the functional updater form: setCount(prev => prev + 1).
Intermediate Level
useReducer and when should you use it over useState?useReducer is ideal when state transitions depend on the previous state and multiple sub-values change together, or when the next state logic is complex.
const initialState = { count: 0, step: 1 };
function reducer(state, action) {
switch (action.type) {
case 'increment': return { ...state, count: state.count + state.step };
case 'setStep': return { ...state, step: action.payload };
default: throw new Error('Unknown action');
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
}useMemo and when should you use it?useMemo memoizes the result of an expensive computation, recomputing it only when dependencies change. It prevents unnecessary recalculations on every render.
import { useMemo } from 'react';
function ProductList({ products, filter }) {
const filtered = useMemo(
() => products.filter(p => p.category === filter),
[products, filter] // only recompute when these change
);
return filtered.map(p => <ProductCard key={p.id} product={p} />);
}Don’t over-optimize — only use useMemo when a profiler shows a real bottleneck. The memoization itself has overhead.
useCallback and how does it differ from useMemo?useCallback(fn, deps) memoizes a function reference. It’s equivalent to useMemo(() => fn, deps). Use it when passing callbacks to memoized child components to prevent unnecessary re-renders.
const handleClick = useCallback(() => {
doSomethingWith(id);
}, [id]); // stable reference unless id changes
// useMemo — memoizes a VALUE
const total = useMemo(() => items.reduce((s, i) => s + i.price, 0), [items]);
// useCallback — memoizes a FUNCTION
const getTotal = useCallback(() => items.reduce((s, i) => s + i.price, 0), [items]);React.memo? How does it work?React.memo is a higher-order component that memoizes a functional component. It skips re-rendering if props haven’t changed (shallow comparison).
const ExpensiveChild = React.memo(function({ value }) {
console.log('rendered');
return <div>{value}</div>;
});
// Custom comparator
const MemoComp = React.memo(Comp, (prev, next) => {
return prev.id === next.id; // return true → skip re-render
});Works best when paired with useCallback/useMemo for stable prop references.
A custom Hook is a function whose name starts with use and that calls other Hooks. It extracts reusable stateful logic from components.
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(r => r.json())
.then(d => { if (!cancelled) setData(d); })
.catch(e => { if (!cancelled) setError(e); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// Usage
const { data, loading } = useFetch('/api/users');Reconciliation is the algorithm React uses to diff the new Virtual DOM tree against the previous one and determine the minimal set of real DOM changes needed.
Key heuristics:
- Elements of different types produce entirely different trees (full subtree rebuild).
- The developer can hint stable identity with the
keyprop. - Same type → React updates props in place, keeping DOM node and children.
React 18 uses the Fiber architecture which makes reconciliation interruptible, enabling concurrent features like Suspense and transitions.
Fiber is React’s internal reconciliation engine (introduced in React 16). It reimplements the reconciler using a linked list of “fiber” units of work, allowing React to pause, resume, abort, and prioritize rendering work.
This enables:
- Concurrent rendering — interruptible renders that keep the UI responsive.
- Suspense & lazy loading — pause rendering until async data or components are ready.
- startTransition — mark non-urgent state updates so urgent updates (typing) stay fast.
React.lazy and Suspense? Write an example.React.lazy lets you code-split a component into a separate bundle loaded on demand. Suspense shows a fallback while it loads.
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
);
}The browser only downloads the Dashboard bundle when it’s first rendered. Useful for route-level code splitting.
Error Boundaries are class components that catch JavaScript errors in their child tree and display a fallback UI instead of crashing the whole app. They must implement static getDerivedStateFromError or componentDidCatch.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
logErrorToService(error, info.componentStack);
}
render() {
if (this.state.hasError)
return <h2>Something went wrong.</h2>;
return this.props.children;
}
}Note: Error boundaries don’t catch errors in event handlers or async code — use regular try/catch for those.
useLayoutEffect Hook? How is it different from useEffect?useLayoutEffect fires synchronously after all DOM mutations but before the browser paints. useEffect fires after the paint.
useLayoutEffect(() => {
// Measure DOM, synchronously update layout
const rect = ref.current.getBoundingClientRect();
setWidth(rect.width);
}); // no flicker — runs before browser paintUse useLayoutEffect when you need to read layout from the DOM and synchronously re-render to prevent visual flicker (e.g., tooltips, measuring elements). For everything else, prefer useEffect to avoid blocking the paint.
Portals render children into a DOM node that exists outside the parent component’s DOM hierarchy, while still keeping them in the React component tree (events bubble normally).
import { createPortal } from 'react-dom';
function Modal({ children }) {
return createPortal(
<div className="modal">{children}</div>,
document.getElementById('modal-root') // DOM outside app root
);
}Common use cases: modals, tooltips, dropdowns — anything that needs to visually escape overflow-hidden or z-index constraints of its parent.
forwardRef and why is it needed?By default, ref cannot be passed as a prop to a functional component. forwardRef lets you expose a ref from a parent to a DOM node inside the child.
const Input = React.forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
function Parent() {
const inputRef = useRef();
return <Input ref={inputRef} />; // ref reaches the <input> DOM node
}Commonly used in design system libraries to give consumers direct DOM access while keeping internal implementation details abstracted.
useEffect with no deps, empty array [], and dependencies?// No dependency array → runs after EVERY render
useEffect(() => { console.log('every render'); });
// Empty array [] → runs ONCE after mount
useEffect(() => { console.log('mounted'); }, []);
// With deps → runs on mount AND when any dep changes
useEffect(() => {
console.log('userId changed');
}, [userId]);The cleanup function returned from useEffect runs before the next effect execution or on unmount — in all three cases.
function LoginForm() {
const [fields, setFields] = useState({ email: '', password: '' });
const [errors, setErrors] = useState({});
function validate() {
const e = {};
if (!fields.email.includes('@')) e.email = 'Invalid email';
if (fields.password.length < 8) e.password = 'Min 8 chars';
return e;
}
function handleSubmit(e) {
e.preventDefault();
const e2 = validate();
if (Object.keys(e2).length) { setErrors(e2); return; }
submitToServer(fields);
}
const change = field => e =>
setFields(prev => ({ ...prev, [field]: e.target.value }));
return (
<form onSubmit={handleSubmit}>
<input value={fields.email} onChange={change('email')} />
{errors.email && <span>{errors.email}</span>}
<input type="password" value={fields.password} onChange={change('password')} />
{errors.password && <span>{errors.password}</span>}
<button type="submit">Login</button>
</form>
);
}The render props pattern involves a component that accepts a function as a prop (or as children), and calls it to determine what to render, sharing logic without inheritance or HOCs.
function MouseTracker({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
return (
<div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}>
{render(pos)}
</div>
);
}
// Usage
<MouseTracker render={({ x, y }) => <p>{x}, {y}</p>} />Hooks have largely replaced render props for sharing logic, but the pattern is still common in libraries like React Router and Formik.
A HOC is a function that takes a component and returns an enhanced component. It’s a compositional pattern for cross-cutting concerns (auth, logging, theming).
function withAuth(WrappedComponent) {
return function AuthGuard(props) {
const { isLoggedIn } = useAuth();
if (!isLoggedIn) return <Redirect to="/login" />;
return <WrappedComponent {...props} />;
};
}
const ProtectedDashboard = withAuth(Dashboard);HOCs should not mutate the wrapped component. Use a display name (AuthGuard.displayName) for better DevTools debugging.
Combining Context and useReducer gives you a lightweight Redux-like global store without external libraries.
const StoreContext = createContext();
function storeReducer(state, action) {
switch (action.type) {
case 'LOGIN': return { ...state, user: action.payload };
case 'LOGOUT': return { ...state, user: null };
default: return state;
}
}
export function StoreProvider({ children }) {
const [state, dispatch] = useReducer(storeReducer, { user: null });
return (
<StoreContext.Provider value={{ state, dispatch }}>
{children}
</StoreContext.Provider>
);
}
export const useStore = () => useContext(StoreContext);useImperativeHandle and when do you use it?useImperativeHandle customizes the instance value exposed to parent refs via forwardRef, allowing you to expose only a limited API instead of the raw DOM node.
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ''; },
// DOM node itself is NOT exposed
}));
return <input ref={inputRef} />;
});
// Parent can call: ref.current.focus() or ref.current.clear()import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/user/42">User</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<UserProfile />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}function PostList() {
const [posts, setPosts] = useState([]);
const [status, setStatus] = useState('idle'); // idle|loading|success|error
const [error, setError] = useState(null);
useEffect(() => {
setStatus('loading');
fetch('/api/posts')
.then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); })
.then(data => { setPosts(data); setStatus('success'); })
.catch(err => { setError(err.message); setStatus('error'); });
}, []);
if (status === 'loading') return <Spinner />;
if (status === 'error') return <p>Error: {error}</p>;
return posts.map(p => <Post key={p.id} post={p} />);
}React.cloneElement and children props?React.cloneElement lets you clone a React element and inject additional props or override existing ones, useful in compound component patterns.
function Tabs({ children, activeTab }) {
return (
<div>
{React.Children.map(children, child =>
React.cloneElement(child, {
isActive: child.props.id === activeTab
})
)}
</div>
);
}
// Each Tab child now receives isActive without the parent knowing its internalsModern alternative: use Context to share state inside compound components without cloneElement.
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) { setResults([]); return; }
const timer = setTimeout(() => {
fetchResults(query).then(setResults);
}, 300); // 300ms debounce
return () => clearTimeout(timer); // cancel if query changes
}, [query]);
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
{results.map(r => <div key={r.id}>{r.title}</div>)}
</>
);
}React DevTools is a browser extension that adds a Components and Profiler panel to browser DevTools.
- Components panel — inspect the component tree, view props/state/hooks, and highlight re-renders.
- Profiler panel — record a session, then see which components rendered, how long each took (in ms), and why they re-rendered. Flame chart shows the render waterfall.
Workflow: Record → interact with the app → stop recording → look for unexpectedly frequent or slow renders → apply memo, useCallback, or structural fixes as needed.
Compound components are a set of components that work together and share implicit state via Context. The parent manages state; children can access it without explicit props.
const AccordionContext = createContext();
function Accordion({ children }) {
const [open, setOpen] = useState(null);
return (
<AccordionContext.Provider value={{ open, setOpen }}>
<div>{children}</div>
</AccordionContext.Provider>
);
}
function Item({ id, children }) {
const { open, setOpen } = useContext(AccordionContext);
return (
<div>
<button onClick={() => setOpen(open === id ? null : id)}>Toggle</button>
{open === id && children}
</div>
);
}
Accordion.Item = Item;
// Usage: <Accordion><Accordion.Item id="a">...</Accordion.Item></Accordion>function InfiniteList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const sentinelRef = useRef();
useEffect(() => {
fetchPage(page).then(data => setItems(prev => [...prev, ...data]));
}, [page]);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) setPage(p => p + 1);
});
if (sentinelRef.current) observer.observe(sentinelRef.current);
return () => observer.disconnect();
}, []);
return (
<>
{items.map(i => <Item key={i.id} data={i} />)}
<div ref={sentinelRef} /> // invisible bottom sentinel
</>
);
}Code splitting breaks your bundle into smaller chunks loaded on demand, reducing initial bundle size and TTI (Time to Interactive).
- Component-level:
React.lazy+ dynamicimport() - Route-level: Lazy-load each route component
- Library-level: Webpack/Vite automatically split node_modules
// Route-level splitting
const Home = lazy(() => import('./routes/Home'));
const Profile = lazy(() => import('./routes/Profile'));
<Suspense fallback={<Spinner/>}>
<Routes>
<Route path="/" element={<Home/>} />
<Route path="/profile" element={<Profile/>} />
</Routes>
</Suspense>React supports full HTML accessibility attributes with camelCase naming. Key practices:
// aria-* attributes stay hyphenated
<button aria-label="Close modal" aria-expanded={isOpen}>✕</button>
// for/htmlFor association
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// Focus management for modals
useEffect(() => { if (isOpen) closeButtonRef.current?.focus(); }, [isOpen]);Tools: eslint-plugin-jsx-a11y, React Aria (Adobe), axe-core DevTools extension.
useId Hook?Introduced in React 18, useId generates a stable unique ID that is consistent between server and client renders — avoiding SSR hydration mismatches.
function FormField({ label }) {
const id = useId(); // e.g. ":r1:"
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}Do not use useId to generate keys for lists — use data IDs for that.
startTransition marks a state update as non-urgent. React will defer it and keep the UI responsive for urgent updates (like typing).
import { startTransition, useTransition } from 'react';
function Search() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function handleChange(e) {
setQuery(e.target.value); // urgent — update input immediately
startTransition(() => {
setResults(computeResults(e.target.value)); // non-urgent
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner/> : results.map(...)}
</>
);
}The standard stack: Vitest or Jest (test runner) + React Testing Library (RTL) for DOM-focused tests + Playwright/Cypress for end-to-end tests.
// Example RTL test
import { render, screen, fireEvent } from '@testing-library/react';
test('increments counter', () => {
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});RTL philosophy: test what the user sees (text, roles) not implementation details (state, refs).
Advanced Level
Concurrent rendering (React 18) lets React prepare multiple versions of the UI simultaneously without blocking the main thread. Renders can be interrupted, paused, and resumed based on priority.
Key APIs:
createRoot— opt into concurrent modestartTransition/useTransition— deprioritize non-urgent updatesuseDeferredValue— defer a value to avoid blocking inputSuspense+ data fetching — show fallbacks while async rendering
Benefit: heavy renders (large lists, complex charts) no longer freeze the UI — React keeps urgent interactions (typing, clicking) buttery smooth.
useDeferredValue? How does it compare to debouncing?useDeferredValue accepts a value and returns a deferred version that “lags behind” to allow more urgent renders to go first.
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
// stale deferredQuery during typing → React renders latest query first
const results = expensiveFilter(deferredQuery);
return results.map(r => <Result key={r.id} {...r} />);
}vs debounce: debounce delays state updates on a fixed timer. useDeferredValue lets React schedule the update based on available CPU time — no artificial delay, and it starts updating as soon as the browser is idle.
With SSR, React renders components to HTML on the server and sends it to the browser. The client then “hydrates” — attaching event listeners to the existing HTML without re-rendering.
// Server (Node.js)
import { renderToString } from 'react-dom/server';
const html = renderToString(<App />);
res.send(`<html><body><div id="root">${html}</div></body></html>`);
// Client
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);Benefits: faster FCP, SEO-friendly. Drawbacks: TTFB increases, server load, hydration complexity. Frameworks: Next.js, Remix.
React Server Components (introduced in React 18, popularized by Next.js 13+ App Router) run exclusively on the server. They can access databases and file systems directly, never ship JS to the client, and reduce bundle size.
- Server Components — async, no state/hooks, zero client JS
- Client Components —
'use client'directive, can use hooks and events - Shared Components — can render as either depending on where they’re imported
// app/page.tsx — Server Component (default in Next.js 13+)
async function Page() {
const data = await db.query('SELECT * FROM posts'); // runs on server only
return data.map(post => <PostCard key={post.id} post={post} />);
}Hydration is the process of attaching React’s event system to server-rendered HTML. React walks the existing DOM and matches it against the Virtual DOM tree. If they don’t match, React throws a hydration error and falls back to client rendering.
Common causes of hydration mismatch:
- Rendering
Date.now()orMath.random()differently server vs client - Using
typeof windowto conditionally render - Third-party scripts modifying the DOM before React hydrates
- Invalid HTML nesting (e.g.
<p><div></div></p>)
Fix: use suppressHydrationWarning for intentional mismatches (e.g., timestamps), or defer rendering until client with useEffect.
Suspense data-fetching model (Suspense for Data Fetching)?A component “suspends” by throwing a Promise. React catches it, shows the nearest Suspense fallback, and retries rendering the component when the Promise resolves.
// Library creates a "resource" that throws a Promise
function wrapPromise(promise) {
let status = 'pending', result;
const p = promise.then(d => { status = 'success'; result = d; })
.catch(e => { status = 'error'; result = e; });
return { read() {
if (status === 'pending') throw p;
if (status === 'error') throw result;
return result;
}};
}
// Component using the resource
function UserProfile({ resource }) {
const user = resource.read(); // throws Promise if not ready
return <div>{user.name}</div>;
}In practice, frameworks like Next.js and libraries like TanStack Query implement this for you.
Virtualization renders only the visible rows, keeping DOM nodes constant regardless of list size. Great for lists of 10,000+ items.
const ROW_HEIGHT = 40;
function VirtualList({ items }) {
const [scrollTop, setScrollTop] = useState(0);
const containerHeight = 400;
const totalHeight = items.length * ROW_HEIGHT;
const startIndex = Math.floor(scrollTop / ROW_HEIGHT);
const visibleCount = Math.ceil(containerHeight / ROW_HEIGHT) + 1;
const visibleItems = items.slice(startIndex, startIndex + visibleCount);
return (
<div
style={{ height: containerHeight, overflowY: 'auto', position: 'relative' }}
onScroll={e => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: totalHeight }}>
{visibleItems.map((item, i) => (
<div
key={item.id}
style={{
position: 'absolute',
top: (startIndex + i) * ROW_HEIGHT,
height: ROW_HEIGHT,
}}
>
{item.name}
</div>
))}
</div>
</div>
);
}In production, use react-window or @tanstack/react-virtual.
Flux is a unidirectional data-flow pattern from Facebook: Action → Dispatcher → Store → View → Action. Redux is an opinionated Flux implementation with a single store, pure reducer functions, and a rich middleware ecosystem.
// Redux flow
store.dispatch({ type: 'counter/increment' }); // Action
// Reducer: (state, action) => newState
// Subscribers re-renderRedux Toolkit (RTK) is now the official, recommended way to use Redux — it uses Immer internally so you can “mutate” state in reducers, and createSlice handles action type boilerplate.
Zustand is a minimal, hook-based state manager. It has almost no boilerplate and works outside React components too.
import { create } from 'zustand';
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
}));
function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}vs Redux: Zustand is far less boilerplate, no Provider needed, subscribes components to only the slice of state they use. Redux RTK remains better for large teams needing strict conventions, time-travel debugging, and powerful middleware.
Systematic approach:
- Profile first — use React DevTools Profiler to find offending components before guessing.
- Memoize components —
React.memoskips re-renders when props are reference-equal. - Stable references —
useCallback/useMemoprevent new object/function refs on each render. - Split Context — separate frequently-changing from infrequently-changing context values.
- Colocate state — push state down; only subtrees that need it re-render.
- Virtualize lists —
react-windowrenders only visible rows. - Lazy load — code-split heavy sections, images, data.
- Transitions — wrap non-urgent updates in
startTransition.
flushSync in React 18?React 18 batches all state updates automatically (even in setTimeout and Promises). flushSync forces React to flush pending updates synchronously inside the callback — useful when you need DOM measurements immediately after a state update.
import { flushSync } from 'react-dom';
flushSync(() => {
setItems([..items, newItem]);
});
// DOM is updated HERE, before the next line
listRef.current.lastChild.scrollIntoView();Use sparingly — overuse hurts performance by defeating batching.
function DnDList({ initialItems }) {
const [items, setItems] = useState(initialItems);
const dragIndex = useRef(null);
function handleDragStart(index) { dragIndex.current = index; }
function handleDrop(dropIndex) {
const updated = [...items];
const [removed] = updated.splice(dragIndex.current, 1);
updated.splice(dropIndex, 0, removed);
setItems(updated);
dragIndex.current = null;
}
return (
<ul>
{items.map((item, i) => (
<li
key={item.id}
draggable
onDragStart={() => handleDragStart(i)}
onDragOver={e => e.preventDefault()}
onDrop={() => handleDrop(i)}
>
{item.label}
</li>
))}
</ul>
);
}For production: use @dnd-kit/core or react-beautiful-dnd for accessibility, touch support, and animation.
A stale closure occurs when a callback captures an old value from a previous render and doesn’t see the current state/props.
// ❌ Bug — count is stale inside setInterval callback
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // always reads count=0
}, 1000);
return () => clearInterval(id);
}, []); // empty deps — effect never re-runs
// ✅ Fix — use functional updater
setCount(prev => prev + 1); // prev is always fresh
// ✅ Alternative — use a ref to track latest value
const countRef = useRef(count);
countRef.current = count;
// inside callback: use countRef.currentAn optimistic update applies a change immediately in the UI before the server confirms it, then rolls back if the server returns an error.
async function toggleLike(postId) {
// 1. Optimistically update UI
setPosts(prev => prev.map(p =>
p.id === postId ? { ...p, liked: !p.liked } : p
));
try {
await api.toggleLike(postId); // 2. Persist on server
} catch {
// 3. Roll back on failure
setPosts(prev => prev.map(p =>
p.id === postId ? { ...p, liked: !p.liked } : p // toggle back
));
toast.error('Failed to update like');
}
}React 19 introduced useOptimistic for a built-in, first-class API for this pattern.
TanStack Query is a server-state management library. It handles: caching, background refetching, deduplication of requests, pagination, infinite scroll, optimistic updates, synchronization, and loading/error states — all declaratively.
import { useQuery, useMutation } from '@tanstack/react-query';
function Posts() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 minutes
});
if (isLoading) return <Spinner />;
if (error) return <Error />;
return data.map(p => <Post key={p.id} post={p} />);
}function useNotifications(userId) {
const [notifications, setNotifications] = useState([]);
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/ws?user=${userId}`);
ws.onmessage = (event) => {
const notif = JSON.parse(event.data);
setNotifications(prev => [notif, ...prev]);
};
ws.onerror = (e) => console.error('WebSocket error', e);
ws.onclose = () => console.log('WebSocket closed');
return () => ws.close(); // cleanup on unmount / userId change
}, [userId]);
return notifications;
}Alternatives: SSE (EventSource), long polling, or libraries like Socket.io / Ably.
The <Profiler> component lets you programmatically measure rendering performance in production builds.
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration, baseDuration) {
sendToAnalytics({ id, phase, actualDuration, baseDuration });
}
<Profiler id="Navigation" onRender={onRenderCallback}>
<Navigation />
</Profiler>Parameters: id (label), phase (mount/update), actualDuration (time for this render), baseDuration (estimated without memo), startTime, commitTime.
function Modal({ isOpen, onClose, title, children }) {
const dialogRef = useRef();
useEffect(() => {
if (!isOpen) return;
dialogRef.current?.focus();
const onKey = e => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [isOpen, onClose]);
if (!isOpen) return null;
return createPortal(
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div ref={dialogRef} tabIndex={-1}>
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose} aria-label="Close">✕</button>
</div>
</div>,
document.body
);
}State normalization stores entity data in a flat map (keyed by ID) rather than nested arrays. This avoids data duplication and makes updates O(1) instead of O(n).
// ❌ Denormalized — hard to update a specific post
{ posts: [{ id: 1, author: { id: 5, name: 'Alice' } }, ...] }
// ✅ Normalized — each entity stored once
{
posts: { ids: [1], entities: { 1: { id: 1, authorId: 5 } } },
users: { ids: [5], entities: { 5: { id: 5, name: 'Alice' } } }
}Redux Toolkit’s createEntityAdapter automates this pattern. TanStack Query handles it automatically via query cache keying.
Key CWV metrics and React-specific fixes:
- LCP (Largest Contentful Paint) — SSR or SSG, preload hero image, eliminate render-blocking resources.
- INP (Interaction to Next Paint) — debounce handlers, use
startTransition, avoid long tasks, virtualize large lists. - CLS (Cumulative Layout Shift) — set explicit dimensions on images/iframes, avoid injecting content above existing content.
// Measure with web-vitals library
import { onINP, onLCP, onCLS } from 'web-vitals';
onINP(metric => sendToAnalytics(metric));
onLCP(metric => sendToAnalytics(metric));use Hook (React 19)?The use Hook (React 19) lets you read the value of a resource — a Promise or a Context — inside render. Unlike other hooks, use can be called inside conditionals and loops.
// Reading a Context with use (equivalent to useContext)
import { use } from 'react';
function Heading({ children }) {
const level = use(LevelContext);
return <{`h${level}`}>{children}</{`h${level}`}>;
}
// Reading a Promise (must be wrapped / cache)
function Comments({ commentsPromise }) {
const comments = use(commentsPromise); // suspends until resolved
return comments.map(c => <Comment key={c.id} comment={c} />);
}Key principles for large-scale React apps:
- Feature-based folder structure — group by domain (
features/auth,features/dashboard) not by type. - Clear layer separation — UI components → hooks/services → API layer.
- Strict module boundaries — use barrel exports and enforce with ESLint import rules.
- Micro-frontend or monorepo — NX/Turborepo for team scalability.
- Design system — shared component library (Storybook) consumed by all features.
- Typed contracts — TypeScript + Zod for runtime validation of API responses.
- Testing pyramid — unit (hooks/utils), integration (RTL), e2e (Playwright).
Server Actions let you call server-side functions directly from client components — without manually writing API routes. They’re marked with 'use server' and can be called from form actions or event handlers.
// actions.ts — runs on server
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.post.create({ data: { title } });
revalidatePath('/posts');
}
// Client component
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>Streaming SSR uses renderToPipeableStream (Node.js) or renderToReadableStream (Edge) to stream HTML to the browser in chunks rather than waiting for the full page to render.
import { renderToPipeableStream } from 'react-dom/server';
res.setHeader('Content-Type', 'text/html');
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() { pipe(res); }, // send shell immediately
onError(err) { console.error(err); }
});Wrapped in Suspense, slow components don’t block the initial shell. The client progressively hydrates chunks as they stream in. Result: faster FCP + TTFB without sacrificing dynamic content.
Micro-frontends decompose a large frontend into independently deployable apps owned by different teams. Common approaches with React:
- Module Federation (Webpack 5 / Rspack) — dynamically load remote components at runtime.
- iframes — strong isolation, simple, but limited UX/communication.
- Custom Elements / Web Components — wrap React apps as standards-based elements.
- Single-SPA — orchestrates multiple framework apps on one page.
// webpack.config.js (Host) — Module Federation
new ModuleFederationPlugin({
remotes: {
cart: 'cart@https://cart.example.com/remoteEntry.js',
},
})
// In host app
const CartWidget = lazy(() => import('cart/CartWidget'));Expert Level
React uses a scheduler (the scheduler package) that assigns lanes and priorities to work. Work is queued in a min-heap and processed in priority order using cooperative scheduling (yielding to the browser event loop).
Priority levels (React 18 lanes):
- SyncLane —
flushSync, legacy mode. Always processes before paint. - InputContinuousLane — pointer/scroll events. Processed before next frame.
- DefaultLane — normal setState. Batch and process asap.
- TransitionLane —
startTransition. Can be interrupted by higher-priority work. - OffscreenLane — pre-rendering hidden content.
The scheduler uses MessageChannel to schedule work asynchronously, yielding every ~5ms to let the browser handle input/paint.
React wraps event handlers in batchedUpdates. In React 17 and earlier, this only applied inside React event handlers. In React 18, batching is automatic everywhere via a mechanism called automatic batching.
Internally: each setState call enqueues an update on the fiber’s updateQueue. React defers the re-render by scheduling work asynchronously. Only after the current execution context ends does React flush the queue and process all enqueued updates together in a single render pass.
Calling flushSync forces immediate synchronous flush of the queue.
The React Compiler (previously codenamed “React Forget”) is a Babel plugin developed by the React team that automatically memoizes components, hooks, and JSX expressions at compile time — eliminating the need for manual useMemo, useCallback, and React.memo.
It uses static analysis to identify values whose referential identity needs to be preserved, then inserts the correct memoization. It understands React’s rules of hooks and can prove safety of optimizations.
Released as part of React 19 with Meta running it in production on Instagram.com before public release. Opt in via babel config or Next.js config option.
React’s reconciler (react-reconciler) is decoupled from the host environment. You implement a “host config” that defines how to create, update, and delete nodes in your custom target.
import Reconciler from 'react-reconciler';
const HostConfig = {
createInstance(type, props) { return { type, props, children: [] }; },
appendChildToContainer(container, child) { container.children.push(child); },
commitUpdate(instance, _, __, ___, newProps) { instance.props = newProps; },
removeChildFromContainer(container, child) { /* ... */ },
supportsMutation: true,
// ... ~30 other required methods
};
const MyRenderer = Reconciler.createContainer(HostConfig);
export function render(element, container) {
MyRenderer.updateContainer(element, container);
}Examples in the wild: React Three Fiber (WebGL/Three.js), React PDF, React Native, Ink (terminal).
Internally, a Context object has a $$typeof symbol and stores the current value on the provider’s fiber during reconciliation.
When a Provider renders, React pushes the new value onto a context stack (a linked list of fiber nodes). When a useContext consumer renders, React walks up the fiber tree to find the nearest matching Provider and reads its current value.
When the Provider’s value changes, React propagates the change by marking all consumers as needing re-render (a “context propagation bailout” scan). This is O(n) in the subtree size, which is why splitting contexts and memoizing consumers matters for performance.
React.memo with objects and functions?React.memo uses shallow reference equality. Every render creates new object/array/function references, so memoization is defeated without useMemo/useCallback.
const Child = React.memo(({ style, onClick }) => <div style={style} onClick={onClick}>...</div>);
// ❌ New object on every Parent render — memo is useless
<Child style={{ color: 'red' }} onClick={() => doThing()} />
// ✅ Stable references
const style = useMemo(() => ({ color: 'red' }), []);
const onClick = useCallback(() => doThing(), []);
<Child style={style} onClick={onClick} />Alternative: use the React Compiler which handles this automatically, or design components to accept primitive props.
Race conditions occur when a user triggers multiple requests and the last one resolves before an earlier one, displaying stale data. Solutions:
// Pattern 1: cleanup flag
useEffect(() => {
let active = true;
fetchData(id).then(data => { if (active) setData(data); });
return () => { active = false; };
}, [id]);
// Pattern 2: AbortController
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(e => { if (e.name !== 'AbortError') setError(e); });
return () => controller.abort();
}, [url]);TanStack Query handles race conditions automatically — it cancels in-flight queries when a newer request supersedes them.
// flags.ts
export const flags = {
newDashboard: Boolean(process.env.NEXT_PUBLIC_FLAG_NEW_DASHBOARD),
};
// Hook
function useFlag(key) {
const { user } = useAuth();
const remoteFlags = useQuery({ queryKey: ['flags', user.id], queryFn: fetchFlags });
return remoteFlags.data?.[key] ?? flags[key] ?? false;
}
// Usage
function App() {
const showNewDash = useFlag('newDashboard');
return showNewDash ? <NewDashboard /> : <OldDashboard />;
}Production systems use services like LaunchDarkly, Statsig, or GrowthBook, which add targeting rules, A/B experimentation, kill switches, and analytics.
// eventBus.ts
type Handler = (data: unknown) => void;
const listeners = new Map<string, Set<Handler>>();
export const eventBus = {
on(event: string, handler: Handler) {
if (!listeners.has(event)) listeners.set(event, new Set());
listeners.get(event)!.add(handler);
return () => listeners.get(event)!.delete(handler);
},
emit(event: string, data?: unknown) {
listeners.get(event)?.forEach(h => h(data));
}
};
// Hook
function useEvent<T>(event: string, handler: (data: T) => void) {
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
return eventBus.on(event, (data) => handlerRef.current(data as T));
}, [event]);
}function useUndoRedo<T>(initial: T) {
const [history, setHistory] = useState<T[]>([initial]);
const [index, setIndex] = useState(0);
const current = history[index];
const set = useCallback((newState: T) => {
const next = history.slice(0, index + 1); // drop future states
setHistory([...next, newState]);
setIndex(next.length);
}, [history, index]);
const undo = () => setIndex(i => Math.max(0, i - 1));
const redo = () => setIndex(i => Math.min(history.length - 1, i + 1));
return { current, set, undo, redo,
canUndo: index > 0,
canRedo: index < history.length - 1
};
}const STEPS = ['info', 'payment', 'confirm'] as const;
function Wizard() {
const [searchParams, setSearchParams] = useSearchParams();
const stepParam = searchParams.get('step');
const stepIndex = STEPS.indexOf((stepParam ?? 'info') as typeof STEPS[0]);
const currentStep = STEPS[Math.max(0, stepIndex)];
const [formData, setFormData] = useState({});
function goTo(step: typeof STEPS[0]) {
setSearchParams({ step });
}
function saveAndNext(data: object) {
setFormData(prev => ({ ...prev, ...data }));
const nextStep = STEPS[stepIndex + 1];
if (nextStep) goTo(nextStep);
}
return (
<>
{currentStep === 'info' && <InfoStep onNext={saveAndNext} />}
{currentStep === 'payment' && <PaymentStep onNext={saveAndNext} />}
{currentStep === 'confirm' && <ConfirmStep data={formData} />}
</>
);
}Web Workers run JS in a background thread, off the main thread. React UI lives on the main thread, but you can offload CPU-heavy work (image processing, search indexing, AI inference) to a worker and communicate via postMessage.
// worker.ts
self.onmessage = ({ data }) => {
const result = heavyComputation(data);
self.postMessage(result);
};
// useWorker.ts
function useWorker(workerPath: string) {
const workerRef = useRef<Worker>();
useEffect(() => {
workerRef.current = new Worker(workerPath, { type: 'module' });
return () => workerRef.current?.terminate();
}, [workerPath]);
const compute = (data: unknown) => new Promise(resolve => {
workerRef.current!.onmessage = ({ data }) => resolve(data);
workerRef.current!.postMessage(data);
});
return { compute };
}act() testing utility and why is it important?act() ensures that all state updates, effects, and re-renders are flushed before you make assertions in tests. Without it, tests may assert on stale DOM state.
import { act } from 'react';
import { render } from '@testing-library/react';
test('loads and displays data', async () => {
await act(async () => {
render(<DataLoader />);
});
// Now state updates + effects have all run
expect(screen.getByText('Data loaded')).toBeInTheDocument();
});React Testing Library wraps all its utilities (render, userEvent, fireEvent) in act automatically, which is why you typically don’t need to call it directly.
A production design system typically combines:
- Design tokens — CSS custom properties or JS objects for color, spacing, typography.
- Primitive components — Box, Text, Stack, Grid with token-based props.
- Compound components — Card, Modal, DataTable built from primitives.
- Storybook — living documentation with interactive playground.
// tokens.ts
export const tokens = {
colors: { primary: '#0066cc', danger: '#dc2626' },
space: [0, 4, 8, 16, 24, 32, 48, 64],
};
// Button with variant system (using vanilla-extract or Tailwind CVA)
const button = cva('rounded font-medium', {
variants: {
intent: {
primary: 'bg-blue-600 text-white',
danger: 'bg-red-600 text-white',
ghost: 'bg-transparent border',
},
size: { sm: 'px-2 py-1 text-sm', lg: 'px-6 py-3 text-lg' },
},
defaultVariants: { intent: 'primary', size: 'sm' },
});Key engineering challenges and solutions:
- Conflict resolution — use Operational Transformation (OT) or CRDTs (Yjs, Automerge) to merge concurrent edits without conflicts.
- Sync — WebSocket for real-time updates; CRDT diffs are small and efficient.
- Awareness — broadcast cursor positions and user presence.
- Offline support — CRDTs can merge divergent offline edits on reconnect.
- React integration — bind Yjs doc changes to React state via
y-reactor custom useSyncExternalStore hook.
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
const ydoc = new Y.Doc();
const provider = new WebsocketProvider('wss://y.example.com', 'room-1', ydoc);
const yText = ydoc.getText('content');
// In component:
useSyncExternalStore(
cb => { yText.observe(cb); return () => yText.unobserve(cb); },
() => yText.toString()
);useSyncExternalStore and when do you need it?useSyncExternalStore (React 18) is the correct way to subscribe to external (non-React) stores inside components. It ensures consistency under concurrent rendering by providing a snapshot mechanism.
import { useSyncExternalStore } from 'react';
function useWindowWidth() {
return useSyncExternalStore(
(callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
},
() => window.innerWidth, // getSnapshot (client)
() => 1024 // getServerSnapshot (SSR)
);
}Use it when integrating with external state stores (Redux, Zustand, RxJS, browser APIs) to avoid tearing — inconsistent state during concurrent renders.
The Offscreen component (API still stabilizing, called Activity in latest React canary) lets React pre-render trees that are not yet visible, or cache them when they’re hidden — without destroying their state.
<Offscreen mode="hidden">
<ExpensiveTab /> // rendered but not visible, state preserved
</Offscreen>Mode options:
visible— normal renderinghidden— rendered off-screen, state preserved, effects pausedmanual— developer controls visibility transitions
Enables: instant tab switching (pre-rendered), keepalive patterns, background rendering. Replaces the display: none hack that destroys React state.
Tearing occurs when React renders a UI snapshot but an external store updates mid-render, causing different components to see different values of the same state — a visually inconsistent UI.
Scenario: Component A reads store version 1 → store updates to version 2 → Component B reads version 2 → they disagree on the same value.
React’s solution: useSyncExternalStore uses a two-phase “getSnapshot” check. After rendering, React verifies that all snapshots are still consistent. If not, it synchronously re-renders — trading some concurrency for consistency.
Libraries using legacy subscription patterns (e.g. old Redux useSelector) are vulnerable to tearing in concurrent mode until they migrate to useSyncExternalStore. RTK Query and modern Zustand handle this correctly.
A production-grade data grid for 100k rows requires multiple techniques in combination:
- Row virtualization — @tanstack/react-virtual or react-window. Only render ~20-50 visible rows.
- Column virtualization — virtual horizontal scrolling for wide tables.
- Memoized row components —
React.memoper row with stable props. - Immutable data structures — structural sharing for efficient diffing.
- Lazy loading — paginated server requests or cursor-based fetching.
- Web Worker offload — filtering, sorting, and aggregation off the main thread.
- Canvas rendering — for extreme performance (AG Grid’s column virtualizer uses canvas for headers).
Production libraries: AG Grid, TanStack Table (headless), react-data-grid.
React 19 ships several transformative features that change how React apps are built:
- React Compiler — automatic memoization;
useMemo/useCallback/React.memobecome largely unnecessary. - Server Components (stable) — zero-JS server-rendered components, direct data access, smaller bundles.
- Server Actions (stable) — async server functions callable from clients, simplifying API route boilerplate.
use()Hook — read Promises and Contexts in render, even inside conditionals.useOptimistic— first-class API for optimistic UI updates.useFormStatus/useFormState— form state management tied to server actions.- Asset loading APIs —
preload,prefetchDNS,preinitfor resource hints directly from components. - Activity (Offscreen) — keep-alive hidden subtrees with paused effects.
The direction: less client JS, better DX, compiler-driven optimization, and deeper server/client boundary awareness baked into the framework.