๐Ÿš€ React Complete Guide

Hooks, Rendering, Re-rendering & Design Patterns โ€” Full Q&A Reference

๐Ÿช

1. useFocus Hook

Track focus state of an element programmatically instead of using CSS :focus-within.

The Goal

function App() {
  const [ref, isFocused] = useFocus()
  return (
    <div>
      <input ref={ref}/>
      {isFocused && <p>focused</p>}
    </div>
  )
}

Implementation

import { useState, useRef, useCallback } from 'react';

function useFocus<T extends HTMLElement = HTMLElement>(): [
  (node: T | null) => void,
  boolean
] {
  const [isFocused, setIsFocused] = useState(false);
  const nodeRef = useRef<T | null>(null);

  // Store handlers in a ref so they're stable across renders
  const handlersRef = useRef({
    focus: () => setIsFocused(true),
    blur: () => setIsFocused(false),
  });

  const ref = useCallback((node: T | null) => {
    const { focus, blur } = handlersRef.current;

    // Cleanup previous node
    if (nodeRef.current) {
      nodeRef.current.removeEventListener('focus', focus);
      nodeRef.current.removeEventListener('blur', blur);
    }

    nodeRef.current = node;

    // Attach to new node
    if (node) {
      node.addEventListener('focus', focus);
      node.addEventListener('blur', blur);
      // Sync initial state
      setIsFocused(document.activeElement === node);
    } else {
      setIsFocused(false);
    }
  }, []);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      const { focus, blur } = handlersRef.current;
      if (nodeRef.current) {
        nodeRef.current.removeEventListener('focus', focus);
        nodeRef.current.removeEventListener('blur', blur);
      }
    };
  }, []);

  return [ref, isFocused];
}

Why These Choices?

ChoiceReason
Callback ref instead of useRefA regular useRef doesn't notify us when content changes. Callback ref is called by React when ref is attached/detached.
Handlers stored in refWe need the exact same function references to remove event listeners. If we created handlers inline, removeEventListener wouldn't find them.
useCallback with [] depsKeeps ref function stable. Without it, React would call ref(null) then ref(element) on every render.
Sync initial focus stateIf element has autoFocus, it's already focused when ref attaches. We check document.activeElement.

2. useIsMounted Hook

Check if component is still mounted (useful for async operations to avoid "setState on unmounted component" warnings).

Implementation

import { useRef, useCallback, useEffect } from 'react';

function useIsMounted(): () => boolean {
  const isMountedRef = useRef(false);

  useEffect(() => {
    isMountedRef.current = true;
    return () => {
      isMountedRef.current = false;
    };
  }, []);

  return useCallback(() => isMountedRef.current, []);
}

// Usage
function App() {
  const isMounted = useIsMounted();
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then((result) => {
      if (isMounted()) {  // Check before setting state
        setData(result);
      }
    });
  }, []);

  return <div>{data}</div>;
}

Why These Choices?

ChoiceReason
useRef not useStateWe don't need re-renders. Just need to track a value silently.
Return a function, not the valueIf we returned the value directly, it would be captured in closures at render time (always true). The function reads the ref at call time, getting the fresh value.
useCallback wrapperEnsures stable reference so it won't cause issues in dependency arrays.
Start with falseComponent isn't fully mounted until effects run.

3. useClickOutside Hook

Detect clicks outside an element (for dropdowns, modals, etc.).

Implementation

import { useRef, useEffect, useCallback } from 'react';

function useClickOutside<T extends HTMLElement = HTMLElement>(
  callback: () => void
): (node: T | null) => void {
  const callbackRef = useRef(callback);
  const nodeRef = useRef<T | null>(null);

  // Always keep the latest callback
  callbackRef.current = callback;

  useEffect(() => {
    const handleClick = (event: MouseEvent) => {
      if (nodeRef.current && !nodeRef.current.contains(event.target as Node)) {
        callbackRef.current();
      }
    };

    document.addEventListener('mousedown', handleClick);
    return () => document.removeEventListener('mousedown', handleClick);
  }, []);

  return useCallback((node: T | null) => {
    nodeRef.current = node;
  }, []);
}

Why These Choices?

ChoiceReason
mousedown instead of clickFires before click, prevents race conditions where dropdown opens and immediately closes.
Callback stored in refAvoids stale closures. Callback might change between renders.
node.contains(target)Returns true if target is the element or any descendant.

4. useDebounce Hook

Debounce a value to limit updates (useful for search inputs).

Implementation

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timeout = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timeout);
  }, [value, delay]);  // Include value in deps!

  return debouncedValue;
}

// Usage
function SearchBox() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    if (debouncedQuery) {
      searchAPI(debouncedQuery);  // Only called 500ms after user stops typing
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

How It Works

User types: "r" โ†’ "re" โ†’ "rea" โ†’ "reac" โ†’ "react"
            โ†“     โ†“      โ†“       โ†“        โ†“
         start  clear  clear   clear    start
         timer  +new   +new    +new     timer
                                           โ†“
                                    500ms passes
                                           โ†“
                                 setDebouncedValue("react")
                                           โ†“
                                    API called once!
๐Ÿ”„
Q1 When you click the button, will "Child rendered" be logged? Why?
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count}
      </button>
      <Child />
    </div>
  );
}

function Child() {
  console.log('Child rendered');
  return <div>I am a child</div>;
}

Yes, "Child rendered" will be logged.

When Parent re-renders (due to count state change), React re-renders all children by default โ€” even if Child receives no props. React doesn't automatically check if a child "needs" to re-render; it just re-renders the entire subtree.

This is why React.memo, useMemo, and useCallback exist โ€” to opt into skipping unnecessary re-renders.

Q2 Now with React.memo wrapping Child, will "Child rendered" be logged when you click the button?
const Child = React.memo(function Child() {
  console.log('Child rendered');
  return <div>I am a child</div>;
});

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count}
      </button>
      <Child />
    </div>
  );
}

No, "Child rendered" will NOT be logged.

React.memo does a shallow comparison of props. When there are no props:

  • Previous props: {}
  • Next props: {}
  • Shallow comparison: equal โœ“

Since "nothing changed" (empty object equals empty object), React skips re-rendering Child.

Q3 Will "Child rendered" be logged when you click the button?
const Child = React.memo(function Child({ style }) {
  console.log('Child rendered');
  return <div style={style}>I am a child</div>;
});

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count}
      </button>
      <Child style={{ color: 'red' }} />
    </div>
  );
}

Yes, "Child rendered" will be logged.

Every render creates a new object { color: 'red' }. Even though the content is identical, React.memo does a shallow comparison which checks reference equality:

{ color: 'red' } === { color: 'red' }  // false (different objects in memory)

Fix: Use useMemo to keep the same reference:

const style = useMemo(() => ({ color: 'red' }), []);
<Child style={style} />
Q4 When you type in the input field, will "Child rendered" be logged?
const Child = React.memo(function Child({ onClick }) {
  console.log('Child rendered');
  return <button onClick={onClick}>Click me</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)} />
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
    </div>
  );
}

No, "Child rendered" will NOT be logged.

useCallback with [] dependencies returns the same function reference across all renders.

  • name state changes โ†’ Parent re-renders
  • handleClick is the same reference (thanks to useCallback)
  • React.memo compares: onClick unchanged โ†’ skip re-render โœ“
Q5 If count is currently 0 and you click the button, what will count be after?
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>+3</button>
    </div>
  );
}

count will be 1 (not 3!)

All three setCount calls capture the same count value (0) from the closure:

setCount(0 + 1);  // โ†’ 1
setCount(0 + 1);  // โ†’ 1
setCount(0 + 1);  // โ†’ 1

React batches these updates, and the final value is 1.

Fix: Use the functional updater:

setCount(c => c + 1);  // 0 โ†’ 1
setCount(c => c + 1);  // 1 โ†’ 2
setCount(c => c + 1);  // 2 โ†’ 3
Q6 How many times will "Effect ran" be logged after initial mount + clicking the button 3 times?
function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Effect ran');
  });

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}

4 times

When useEffect has no dependency array, it runs after every render:

  • Initial mount โ†’ 1 log
  • 3 clicks โ†’ 3 more logs
  • Total: 4
Dependency ArrayWhen Effect Runs
NoneEvery render
[]Only on mount
[a, b]Mount + when a or b changes
๐Ÿ“ Questions 7-25

The full quiz continues with 19 more questions covering: children prop with React.memo, lazy initialization, bailout behavior, key prop gotchas, context re-renders, refs timing, stale closures, useLayoutEffect, automatic batching, and more.

๐Ÿ—๏ธ

Pattern 1: Compound Components

Components that work together, sharing implicit state via Context. Like native <select> and <option>.

โŒ Props Explosion
<Select
  options={[
    { value: 'apple', label: 'Apple', icon: <AppleIcon /> },
    { type: 'divider' },
    { type: 'group', label: 'Tropical', children: [...] },
  ]}
/>
โœ… Compound Components
<Select>
  <Select.Option value="apple">
    <AppleIcon /> Apple
  </Select.Option>
  <Select.Divider />
  <Select.Group label="Tropical">
    <Select.Option value="banana">Banana</Select.Option>
  </Select.Group>
</Select>

Implementation (Tabs Example)

const TabsContext = createContext(null);

function Tabs({ defaultValue, children, onChange }) {
  const [selected, setSelected] = useState(defaultValue);

  const handleSelect = (value) => {
    setSelected(value);
    onChange?.(value);
  };

  return (
    <TabsContext.Provider value={{ selected, onSelect: handleSelect }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function Tab({ value, children }) {
  const { selected, onSelect } = useContext(TabsContext);
  const isActive = selected === value;

  return (
    <button
      className={isActive ? 'tab active' : 'tab'}
      onClick={() => onSelect(value)}
    >
      {children}
    </button>
  );
}

function Panel({ value, children }) {
  const { selected } = useContext(TabsContext);
  if (selected !== value) return null;
  return <div className="tab-panel">{children}</div>;
}

// Attach sub-components
Tabs.Tab = Tab;
Tabs.Panel = Panel;
๐Ÿ’ก Libraries using this

Radix UI, Headless UI, Chakra UI, Reach UI, MUI


Pattern 2: Render Props

Pass a function as children to let consumers decide what to render with the data.

function MouseTracker({ children }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);

  // Call the function with data
  return children(position);
}

// Different UIs, same logic:
<MouseTracker>
  {({ x, y }) => <p>Position: {x}, {y}</p>}
</MouseTracker>

<MouseTracker>
  {({ x, y }) => (
    <div style={{ position: 'fixed', left: x + 10, top: y + 10 }}>
      Tooltip follows mouse!
    </div>
  )}
</MouseTracker>
๐Ÿ“ Modern alternative

Custom hooks are preferred today. Render props still useful for libraries like Formik.


Pattern 3: Custom Hooks

Extract and reuse stateful logic. Must start with use.

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// Composing hooks
function useTheme() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const isDark = theme === 'dark';
  const toggle = useCallback(() => {
    setTheme(t => t === 'light' ? 'dark' : 'light');
  }, []);
  return { theme, isDark, toggle };
}

Pattern 4: Provider Pattern

Encapsulate state + logic + actions in a provider with custom hook.

const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  const login = async (email, password) => {
    const user = await loginAPI(email, password);
    setUser(user);
  };

  const logout = async () => {
    await logoutAPI();
    setUser(null);
  };

  const value = {
    user,
    loading,
    isAuthenticated: !!user,
    login,
    logout,
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

// Usage
<AuthProvider><App /></AuthProvider>

function Navbar() {
  const { user, logout } = useAuth();
  return <button onClick={logout}>Logout {user.name}</button>;
}

Pattern 5: Error Boundary

Catch JavaScript errors in child components (must be class component).

class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Caught:', error);
    // Send to Sentry, etc.
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

<ErrorBoundary fallback={<p>Failed to load.</p>}>
  <MyComponent />
</ErrorBoundary>
โœ… CaughtโŒ Not Caught
Render errorsEvent handlers
Lifecycle methodsAsync code (setTimeout, fetch)

Pattern 6: Polymorphic Components

Change which element is rendered using an as prop.

function Button({ as: Component = 'button', children, ...props }) {
  return <Component {...props}>{children}</Component>;
}

<Button onClick={handleClick}>Click me</Button>
// Renders: <button>Click me</button>

<Button as="a" href="/about">About</Button>
// Renders: <a href="/about">About</a>

<Button as={Link} to="/about">About</Button>
// Renders: <Link to="/about">About</Link>

Pattern 7: Higher-Order Components (HOC)

A function that takes a component and returns a new enhanced component.

// HOC that adds loading state
function withLoading(WrappedComponent) {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) {
      return <div className="spinner">Loading...</div>;
    }
    return <WrappedComponent {...props} />;
  };
}

// Usage
const UserListWithLoading = withLoading(UserList);

<UserListWithLoading 
  isLoading={loading} 
  users={users} 
/>
๐Ÿ“ Modern Alternative

Custom hooks are generally preferred today. HOCs still useful with Redux connect().


Pattern 8: Controlled vs Uncontrolled Components

Controlled: React state is the source of truth. Uncontrolled: DOM holds the state.

โœ… Controlled
function ControlledInput() {
  const [value, setValue] = useState('');
  
  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}
๐Ÿ“‹ Uncontrolled
function UncontrolledInput() {
  const inputRef = useRef(null);
  
  const handleSubmit = () => {
    console.log(inputRef.current.value);
  };
  
  return <input ref={inputRef} defaultValue="" />;
}
Use Controlled WhenUse Uncontrolled When
Need real-time validationSimple forms, file inputs
Conditional disablingIntegration with non-React code
Format input on changePerformance-critical forms

Pattern 9: Container/Presentational

Separate data/logic (Container) from UI rendering (Presentational).

// Presentational: Only UI, receives props
function UserCard({ name, avatar, onFollow }) {
  return (
    <div className="user-card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <button onClick={onFollow}>Follow</button>
    </div>
  );
}

// Container: Handles data & logic
function UserCardContainer({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);
  
  const handleFollow = () => followUser(userId);
  
  if (!user) return <Loading />;
  
  return (
    <UserCard
      name={user.name}
      avatar={user.avatar}
      onFollow={handleFollow}
    />
  );
}

Pattern 10: State Reducer Pattern

Let consumers customize state updates by passing their own reducer.

function useToggle({ reducer = (state, action) => action.changes } = {}) {
  const [on, setOn] = useState(false);
  
  function dispatch(action) {
    const changes = { on: !on };
    const newState = reducer({ on }, { ...action, changes });
    setOn(newState.on);
  }
  
  return { on, toggle: () => dispatch({ type: 'toggle' }) };
}

// Consumer can customize behavior
function App() {
  const { on, toggle } = useToggle({
    reducer: (state, action) => {
      // Prevent turning off after 3 toggles
      if (toggleCount >= 3 && action.changes.on === false) {
        return state; // Don't change
      }
      return action.changes;
    }
  });
}
๐Ÿ’ก Used By

Downshift library uses this pattern extensively for customizable autocomplete.


Pattern 11: Props Getter Pattern

Return functions that bundle complex props together for ease of use.

function useDropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedIndex, setSelectedIndex] = useState(-1);
  
  // Bundle all trigger props together
  const getTriggerProps = (props = {}) => ({
    'aria-expanded': isOpen,
    'aria-haspopup': 'listbox',
    onClick: () => setIsOpen(!isOpen),
    ...props, // Allow overrides
  });
  
  // Bundle all menu props together
  const getMenuProps = (props = {}) => ({
    role: 'listbox',
    'aria-activedescendant': selectedIndex,
    hidden: !isOpen,
    ...props,
  });
  
  return { isOpen, getTriggerProps, getMenuProps };
}

// Usage - clean and accessible
function Dropdown() {
  const { getTriggerProps, getMenuProps } = useDropdown();
  
  return (
    <>
      <button {...getTriggerProps()}>Menu</button>
      <ul {...getMenuProps()}>...</ul>
    </>
  );
}

Pattern 12: Slot Pattern

Named content areas that parent can fill (like Vue slots).

function Card({ header, children, footer }) {
  return (
    <div className="card">
      {header && <div className="card-header">{header}</div>}
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}

// Usage
<Card
  header={<h2>Title</h2>}
  footer={<button>Save</button>}
>
  <p>Card content goes here</p>
</Card>
๐Ÿ’ก Next.js Layouts

Next.js App Router uses slots for parallel routes and layouts.


Pattern 13: Forwarding Refs

Pass refs through wrapper components to the underlying DOM element.

// Without forwardRef - ref would attach to FancyButton, not button
const FancyButton = forwardRef(function FancyButton(props, ref) {
  return (
    <button ref={ref} className="fancy-button">
      {props.children}
    </button>
  );
});

// Usage - ref now points to the actual button element
function Form() {
  const buttonRef = useRef(null);
  
  useEffect(() => {
    buttonRef.current.focus(); // Works!
  }, []);
  
  return <FancyButton ref={buttonRef}>Click me</FancyButton>;
}

Pattern 14: Portal Pattern

Render children into a DOM node outside the parent hierarchy (modals, tooltips).

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;
  
  // Render into document.body instead of parent
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        <button className="modal-close" onClick={onClose}>ร—</button>
        {children}
      </div>
    </div>,
    document.body
  );
}

// Usage - renders at body level, not inside Button
function App() {
  const [showModal, setShowModal] = useState(false);
  
  return (
    <div className="app">
      <button onClick={() => setShowModal(true)}>Open</button>
      <Modal isOpen={showModal} onClose={() => setShowModal(false)}>
        <h2>Modal Content</h2>
      </Modal>
    </div>
  );
}

Pattern 15: Optimistic Updates

Update UI immediately, then sync with server. Rollback on failure.

function TodoList() {
  const [todos, setTodos] = useState([]);
  
  const toggleTodo = async (id) => {
    // 1. Save previous state for rollback
    const previousTodos = todos;
    
    // 2. Optimistically update UI immediately
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
    
    try {
      // 3. Sync with server
      await api.toggleTodo(id);
    } catch (error) {
      // 4. Rollback on failure
      setTodos(previousTodos);
      toast.error('Failed to update todo');
    }
  };
}
๐Ÿ’ก React Query / TanStack Query

Provides built-in onMutate, onError, onSettled for optimistic updates.


Pattern 16: Suspense for Data Fetching

Declarative loading states with Suspense boundaries.

// Wrapper that throws promise while loading
function fetchData(url) {
  let status = 'pending';
  let result;
  
  const promise = fetch(url)
    .then(r => r.json())
    .then(data => { status = 'success'; result = data; })
    .catch(err => { status = 'error'; result = err; });
  
  return {
    read() {
      if (status === 'pending') throw promise;
      if (status === 'error') throw result;
      return result;
    }
  };
}

const userResource = fetchData('/api/user');

function UserProfile() {
  const user = userResource.read(); // Suspends if pending
  return <h1>{user.name}</h1>;
}

// Usage with Suspense boundary
<Suspense fallback={<Skeleton />}>
  <UserProfile />
</Suspense>

Pattern 17: Lazy Loading / Code Splitting

Load components on demand to reduce initial bundle size.

import { lazy, Suspense } from 'react';

// Lazy load heavy components
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
const Analytics = lazy(() => 
  import('./Analytics').then(module => ({ default: module.Analytics }))
);

function App() {
  return (
    <Suspense fallback={<PageLoader />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>
    </Suspense>
  );
}

// Preload on hover for better UX
<Link 
  to="/dashboard" 
  onMouseEnter={() => import('./Dashboard')}
>
  Dashboard
</Link>

๐Ÿ“‹ All 17 Patterns Summary

#PatternUse CaseLibraries
1Compound ComponentsFlexible, related UIRadix, Headless UI
2Render PropsShare logic with custom renderingFormik, Downshift
3HOCEnhance componentsRedux (connect)
4Controlled/UncontrolledForm handlingReact Hook Form
5ProviderGlobal stateRedux, React Query
6Custom HooksReusable logicSWR, React Query
7Container/PresentationalSeparate logic from UIStorybook libs
8State ReducerCustomizable behaviorDownshift
9Props GetterBundle complex propsReact Table
10SlotContent injectionNext.js layouts
11Error BoundaryGraceful errorsreact-error-boundary
12PolymorphicFlexible element typeChakra UI
13Forwarding RefsPass refs throughAll UI libs
14PortalRender outside parentRadix, Headless UI
15Optimistic UpdatesInstant feedbackReact Query
16SuspenseDeclarative loadingReact Query
17Lazy LoadingCode splittingReact.lazy