Get Started
Mid-Level Mastery: 50+ Q&A

50+ Advanced Mid-Level React Interview Questions (2026)

Bridge the gap between junior and senior. 50+ challenging React interview questions for mid-level developers covering architecture, testing, and performance.

interview-prep

1. Performance Optimization <a name="performance-optimization"></a>

1. Explain the React Profiler API and how you'd use it to identify performance bottlenecks

jsx
// Example of Profiler usage
import { Profiler } from 'react';

function App() {
  const onRender = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
    console.log({
      id,
      phase,
      actualDuration,
      baseDuration,
      startTime,
      commitTime
    });
  };

  return (
    <Profiler id="App" onRender={onRender}>
      <YourComponent />
    </Profiler>
  );
}

Key Points: The Profiler measures component rendering frequency and cost, helping identify expensive components, unnecessary re-renders, and optimization opportunities.

2. How does React.memo differ from useMemo and when should you use each?

jsx
// React.memo - Component memoization
const MemoizedComponent = React.memo(Component, (prevProps, nextProps) => {
  return prevProps.value === nextProps.value;
});

// useMemo - Value memoization
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

// useCallback - Function memoization
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

Key Differences: React.memo memoizes entire components, useMemo memoizes computed values, and useCallback memoizes functions. Use React.memo for expensive components, useMemo for expensive calculations, and useCallback for passing stable references.

3. Implement virtualization for a list with 100,000 items

jsx
import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }) {
  const parentRef = useRef();
  
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
    overscan: 5,
  });

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
        {virtualizer.getVirtualItems().map(virtualItem => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            {items[virtualItem.index]}
          </div>
        ))}
      </div>
    </div>
  );
}

4. Explain code splitting patterns in React and their impact on performance

jsx
// Dynamic imports with React.lazy
const LazyComponent = React.lazy(() => import('./LazyComponent'));

// Route-based splitting
const Home = React.lazy(() => import('./Home'));
const About = React.lazy(() => import('./About'));

// Component-based splitting
const HeavyFeature = React.lazy(() => import('./HeavyFeature'));

// Library splitting using import()
const loadLibrary = async () => {
  const module = await import('heavy-library');
  return module.default;
};

5. How would you implement debouncing and throttling in React hooks?

jsx
// Custom useDebounce hook
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Custom useThrottle hook
function useThrottle(value, limit) {
  const [throttledValue, setThrottledValue] = useState(value);
  const lastRan = useRef(Date.now());

  useEffect(() => {
    const handler = setTimeout(() => {
      if (Date.now() - lastRan.current >= limit) {
        setThrottledValue(value);
        lastRan.current = Date.now();
      }
    }, limit - (Date.now() - lastRan.current));

    return () => {
      clearTimeout(handler);
    };
  }, [value, limit]);

  return throttledValue;
}

2. State Management & Hooks <a name="state-management"></a>

6. Implement a custom useReducer with middleware support

jsx
function useReducerWithMiddleware(reducer, initialState, middlewares = []) {
  const [state, setState] = useState(initialState);
  const stateRef = useRef(state);

  const dispatch = useCallback(async (action) => {
    // Run middleware before reducer
    let currentAction = action;
    
    for (const middleware of middlewares) {
      currentAction = await middleware(currentAction, stateRef.current);
      if (!currentAction) return; // Middleware can stop propagation
    }

    // Apply reducer
    const newState = reducer(stateRef.current, currentAction);
    stateRef.current = newState;
    setState(newState);
  }, [reducer]);

  return [state, dispatch];
}

// Example middleware
const loggerMiddleware = (action, state) => {
  console.log('Dispatching:', action, 'Current state:', state);
  return action;
};

const thunkMiddleware = (action, state) => {
  if (typeof action === 'function') {
    return action(dispatch, () => state);
  }
  return action;
};

7. How does React Query differ from traditional state management?

jsx
// Traditional Redux approach
const fetchData = () => async (dispatch) => {
  dispatch({ type: 'FETCH_START' });
  try {
    const response = await api.getData();
    dispatch({ type: 'FETCH_SUCCESS', payload: response });
  } catch (error) {
    dispatch({ type: 'FETCH_ERROR', error });
  }
};

// React Query approach
const { data, isLoading, error } = useQuery({
  queryKey: ['data'],
  queryFn: () => api.getData(),
  staleTime: 5 * 60 * 1000, // 5 minutes
  cacheTime: 10 * 60 * 1000, // 10 minutes
});

Key Differences: React Query handles server state (async data) with built-in caching, background updates, and synchronization, while traditional state management focuses on client state.

8. Implement a global state manager using Context + useReducer without performance issues

jsx
const StoreContext = React.createContext();
const StoreDispatchContext = React.createContext();

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  // Memoize context value to prevent unnecessary re-renders
  const stateContextValue = useMemo(() => state, [state]);
  
  return (
    <StoreContext.Provider value={stateContextValue}>
      <StoreDispatchContext.Provider value={dispatch}>
        {children}
      </StoreDispatchContext.Provider>
    </StoreContext.Provider>
  );
}

// Custom hooks for optimized consumption
function useStore(selector) {
  const state = useContext(StoreContext);
  
  return useMemo(() => selector(state), [state, selector]);
}

function useDispatch() {
  return useContext(StoreDispatchContext);
}

// Usage with selector pattern
const user = useStore(state => state.user);
const dispatch = useDispatch();

9. What are the performance implications of useState vs useReducer?

jsx
// useState - Good for simple, independent state
const [count, setCount] = useState(0);
const [name, setName] = useState('');

// useReducer - Better for complex, interrelated state
const initialState = { count: 0, user: null, loading: false };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'setUser':
      return { ...state, user: action.payload };
    case 'setLoading':
      return { ...state, loading: action.payload };
    default:
      return state;
  }
}

Performance Considerations: useReducer is better when state updates are complex or multiple state values change together, as it batches updates and reduces re-renders.

10. Implement a bidirectional data flow between parent and deeply nested children

jsx
// Using Context + Callbacks
const FormContext = React.createContext();

function Form({ children, onSubmit }) {
  const [values, setValues] = useState({});
  
  const updateField = useCallback((name, value) => {
    setValues(prev => ({ ...prev, [name]: value }));
  }, []);

  const contextValue = useMemo(() => ({
    values,
    updateField,
    submit: onSubmit
  }), [values, updateField, onSubmit]);

  return (
    <FormContext.Provider value={contextValue}>
      <form>{children}</form>
    </FormContext.Provider>
  );
}

// Deeply nested component
function DeepField({ name }) {
  const { values, updateField } = useContext(FormContext);
  
  return (
    <input
      value={values[name] || ''}
      onChange={(e) => updateField(name, e.target.value)}
    />
  );
}

3. Component Design Patterns <a name="component-patterns"></a>

11. Implement a compound component pattern for a Tab system

jsx
const TabContext = React.createContext();

function Tabs({ children, defaultActive }) {
  const [activeTab, setActiveTab] = useState(defaultActive);
  
  return (
    <TabContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabContext.Provider>
  );
}

function TabList({ children }) {
  const { activeTab, setActiveTab } = useContext(TabContext);
  
  return (
    <div className="tab-list">
      {React.Children.map(children, (child, index) =>
        React.cloneElement(child, {
          isActive: activeTab === index,
          onClick: () => setActiveTab(index),
        })
      )}
    </div>
  );
}

function Tab({ children, isActive, onClick }) {
  return (
    <button
      className={`tab ${isActive ? 'active' : ''}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

function TabPanel({ children, index }) {
  const { activeTab } = useContext(TabContext);
  
  if (activeTab !== index) return null;
  return <div className="tab-panel">{children}</div>;
}

// Usage
<Tabs defaultActive={0}>
  <TabList>
    <Tab>First</Tab>
    <Tab>Second</Tab>
  </TabList>
  <TabPanel index={0}>Content 1</TabPanel>
  <TabPanel index={1}>Content 2</TabPanel>
</Tabs>

12. Create a render props component for handling API calls

jsx
function Fetch({ url, render, children }) {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null,
  });

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const data = await response.json();
        setState({ data, loading: false, error: null });
      } catch (error) {
        setState({ data: null, loading: false, error });
      }
    };

    fetchData();
  }, [url]);

  // Support both render prop and function as children
  if (render) return render(state);
  if (typeof children === 'function') return children(state);
  
  return null;
}

// Usage
<Fetch url="/api/data">
  {({ data, loading, error }) => (
    loading ? <Spinner /> :
    error ? <Error message={error.message} /> :
    <DataDisplay data={data} />
  )}
</Fetch>

13. Implement a Higher Order Component (HOC) for authentication

jsx
function withAuth(WrappedComponent, requiredRoles = []) {
  return function AuthenticatedComponent(props) {
    const { user, loading } = useAuth();
    const location = useLocation();
    const navigate = useNavigate();

    useEffect(() => {
      if (!loading && !user) {
        navigate('/login', { state: { from: location } });
      }
      
      if (!loading && user && requiredRoles.length > 0) {
        const hasRole = requiredRoles.some(role => user.roles.includes(role));
        if (!hasRole) {
          navigate('/unauthorized');
        }
      }
    }, [user, loading, navigate, location]);

    if (loading) return <LoadingSpinner />;
    if (!user) return null;
    
    // Check roles if specified
    if (requiredRoles.length > 0) {
      const hasRole = requiredRoles.some(role => user.roles.includes(role));
      if (!hasRole) return null;
    }

    return <WrappedComponent {...props} user={user} />;
  };
}

// Usage
const AdminDashboard = withAuth(Dashboard, ['admin']);

14. Create a custom hook for infinite scrolling

jsx
function useInfiniteScroll(fetchMore, options = {}) {
  const {
    threshold = 100,
    rootMargin = '0px',
    enabled = true,
  } = options;

  const observerRef = useRef();
  const [isFetching, setIsFetching] = useState(false);
  const lastElementRef = useRef();

  useEffect(() => {
    if (!enabled) return;

    const observer = new IntersectionObserver(
      async (entries) => {
        const target = entries[0];
        if (target.isIntersecting && !isFetching) {
          setIsFetching(true);
          try {
            await fetchMore();
          } finally {
            setIsFetching(false);
          }
        }
      },
      {
        root: null,
        rootMargin,
        threshold,
      }
    );

    observerRef.current = observer;

    if (lastElementRef.current) {
      observer.observe(lastElementRef.current);
    }

    return () => {
      if (observerRef.current) {
        observerRef.current.disconnect();
      }
    };
  }, [enabled, fetchMore, isFetching, rootMargin, threshold]);

  const setLastElementRef = useCallback((node) => {
    if (observerRef.current && lastElementRef.current) {
      observerRef.current.unobserve(lastElementRef.current);
    }

    lastElementRef.current = node;

    if (node && observerRef.current) {
      observerRef.current.observe(node);
    }
  }, []);

  return { setLastElementRef, isFetching };
}

15. Implement error boundaries with recovery capabilities

jsx
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null,
      retryCount: 0,
    };
  }

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

  componentDidCatch(error, errorInfo) {
    this.setState({ errorInfo });
    // Log to error reporting service
    logErrorToService(error, errorInfo);
  }

  handleRetry = () => {
    this.setState(prevState => ({
      hasError: false,
      error: null,
      errorInfo: null,
      retryCount: prevState.retryCount + 1,
    }));
  };

  handleReset = () => {
    this.setState({
      hasError: false,
      error: null,
      errorInfo: null,
      retryCount: 0,
    });
    // Reset app state if needed
    this.props.onReset?.();
  };

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          {this.props.fallback ? (
            this.props.fallback({
              error: this.state.error,
              errorInfo: this.state.errorInfo,
              retryCount: this.state.retryCount,
              onRetry: this.handleRetry,
              onReset: this.handleReset,
            })
          ) : (
            <>
              <details>
                <summary>Error Details</summary>
                <pre>{this.state.error?.toString()}</pre>
                <pre>{this.state.errorInfo?.componentStack}</pre>
              </details>
              <button onClick={this.handleRetry}>
                Retry ({this.state.retryCount + 1}/3)
              </button>
              <button onClick={this.handleReset}>Reset</button>
            </>
          )}
        </div>
      );
    }

    return this.props.children;
  }
}

// Usage with recovery
<ErrorBoundary
  onReset={() => resetAppState()}
  fallback={({ error, onRetry, onReset }) => (
    <ErrorRecoveryUI
      error={error}
      onRetry={onRetry}
      onReset={onReset}
    />
  )}
>
  <UnstableComponent />
</ErrorBoundary>

4. Advanced Hooks & Custom Hooks <a name="advanced-hooks"></a>

16. Create a usePrevious hook that tracks previous values

jsx
function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

// Enhanced version with multiple previous values
function usePreviousValues(value, count = 1) {
  const ref = useRef([]);
  
  useEffect(() => {
    ref.current = [value, ...ref.current].slice(0, count);
  }, [value, count]);
  
  return ref.current;
}

17. Implement useTransition for non-urgent state updates

jsx
function DelayedUpdateComponent() {
  const [isPending, startTransition] = useTransition();
  const [query, setQuery] = useState('');
  const [filteredData, setFilteredData] = useState([]);
  const data = useData();

  const handleSearch = (e) => {
    const value = e.target.value;
    setQuery(value); // Urgent update
    
    // Mark non-urgent state update
    startTransition(() => {
      const filtered = data.filter(item =>
        item.name.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredData(filtered); // Can be interrupted
    });
  };

  return (
    <div>
      <input value={query} onChange={handleSearch} />
      {isPending && <Spinner />}
      <DataList data={filteredData} />
    </div>
  );
}

18. Create a useWebSocket hook with reconnection logic

jsx
function useWebSocket(url, options = {}) {
  const {
    onMessage,
    onOpen,
    onClose,
    onError,
    reconnectAttempts = 3,
    reconnectInterval = 3000,
  } = options;

  const wsRef = useRef();
  const reconnectCountRef = useRef(0);
  const [status, setStatus] = useState('connecting');
  const [lastMessage, setLastMessage] = useState(null);

  const connect = useCallback(() => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      return;
    }

    const ws = new WebSocket(url);
    wsRef.current = ws;

    ws.onopen = (event) => {
      setStatus('connected');
      reconnectCountRef.current = 0;
      onOpen?.(event);
    };

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setLastMessage(data);
      onMessage?.(data);
    };

    ws.onclose = (event) => {
      setStatus('disconnected');
      onClose?.(event);

      // Attempt reconnection
      if (reconnectCountRef.current < reconnectAttempts) {
        reconnectCountRef.current += 1;
        setTimeout(connect, reconnectInterval);
      }
    };

    ws.onerror = (error) => {
      setStatus('error');
      onError?.(error);
    };

    return () => {
      ws.close();
    };
  }, [url, onMessage, onOpen, onClose, onError, reconnectAttempts, reconnectInterval]);

  useEffect(() => {
    const cleanup = connect();
    
    return () => {
      cleanup?.();
    };
  }, [connect]);

  const send = useCallback((data) => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify(data));
    } else {
      throw new Error('WebSocket is not connected');
    }
  }, []);

  const close = useCallback(() => {
    wsRef.current?.close();
  }, []);

  return { send, close, status, lastMessage };
}

19. Implement useSyncExternalStore for external store integration

jsx
function useExternalStore(store, getSnapshot, getServerSnapshot) {
  const [snapshot, setSnapshot] = useState(() => getSnapshot(store));

  useEffect(() => {
    const handleStoreChange = () => {
      setSnapshot(getSnapshot(store));
    };

    // Subscribe to store changes
    const unsubscribe = store.subscribe(handleStoreChange);
    
    // Initial sync
    handleStoreChange();

    return unsubscribe;
  }, [store, getSnapshot]);

  return snapshot;
}

// React 18 version
function useExternalStoreReact18(store, getSnapshot) {
  return useSyncExternalStore(
    store.subscribe,
    () => getSnapshot(store),
    // Server snapshot for SSR
    () => getSnapshot(store)
  );
}

// Example with Redux-like store
const store = {
  state: { count: 0 },
  listeners: new Set(),
  
  getState() {
    return this.state;
  },
  
  subscribe(listener) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  },
  
  dispatch(action) {
    // Update state
    this.state = reducer(this.state, action);
    
    // Notify listeners
    this.listeners.forEach(listener => listener());
  }
};

20. Create useId for generating unique IDs in SSR

jsx
// Custom implementation for React < 18
function useId(prefix = 'id') {
  const idRef = useRef();

  if (typeof window === 'undefined') {
    // SSR: Generate deterministic ID
    if (!idRef.current) {
      idRef.current = `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
    }
  } else {
    // CSR: Use React's useId or fallback
    if (!idRef.current) {
      idRef.current = `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
    }
  }

  return idRef.current;
}

// React 18 native version
import { useId } from 'react';

function FormField({ label }) {
  const id = useId();
  
  return (
    <>
      <label htmlFor={`${id}-input`}>{label}</label>
      <input id={`${id}-input`} />
      <p id={`${id}-description`}>Description</p>
    </>
  );
}

5. Rendering & Lifecycle <a name="rendering-lifecycle"></a>

21. Explain React's commit phase vs render phase

jsx
// Render Phase (can be async, can be interrupted)
function MyComponent() {
  // This runs during render phase
  const [state, setState] = useState(0);
  
  // Effects are scheduled here but run later
  useEffect(() => {
    // Runs during commit phase
    console.log('Effect ran');
  }, [state]);
  
  // Return JSX (render phase)
  return <div>{state}</div>;
}

// Commit Phase (synchronous, cannot be interrupted)
class Component extends React.Component {
  componentDidMount() {
    // Runs during commit phase
    // Can safely perform DOM operations
    this.node.focus();
  }
  
  componentDidUpdate() {
    // Runs during commit phase
    // DOM is updated at this point
  }
  
  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Runs right before commit phase
    // Can capture DOM state
    return this.node.scrollHeight;
  }
}

22. What are keys and how do they affect reconciliation?

jsx
// Problem: Without keys, React re-creates elements
function ListWithoutKeys({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        // ❌ Using index as key can cause issues
        <li key={index}>{item.text}</li>
      ))}
    </ul>
  );
}

// Solution: Stable, unique keys
function ListWithKeys({ items }) {
  return (
    <ul>
      {items.map(item => (
        // ✅ Use unique ID from data
        <li key={item.id}>{item.text}</li>
      ))}
    </ul>
  );
}

// Key behaviors:
// - Same key + same type = reuse element
// - Same key + different type = destroy/create
// - Different key = destroy/create

23. Implement shouldComponentUpdate with React.memo

jsx
// Class component with shouldComponentUpdate
class ExpensiveComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // Custom comparison logic
    if (this.props.value !== nextProps.value) {
      return true;
    }
    if (this.props.user?.id !== nextProps.user?.id) {
      return true;
    }
    return false;
  }
  
  render() {
    return <div>{this.props.value}</div>;
  }
}

// Functional equivalent with React.memo
const ExpensiveComponent = React.memo(
  function ExpensiveComponent({ value, user }) {
    return <div>{value}</div>;
  },
  // Custom comparison function
  (prevProps, nextProps) => {
    if (prevProps.value !== nextProps.value) {
      return false; // Should update
    }
    if (prevProps.user?.id !== nextProps.user?.id) {
      return false; // Should update
    }
    return true; // Should NOT update
  }
);

24. What is hydration and how does it work in SSR?

jsx
// Server-side render
import { renderToString } from 'react-dom/server';

const html = renderToString(<App />);
// Returns: '<div id="root">...</div>'

// Client-side hydrate
import { hydrateRoot } from 'react-dom/client';

const root = hydrateRoot(
  document.getElementById('root'),
  <App />
);

// Hydration process:
// 1. Server renders HTML
// 2. HTML sent to client
// 3. React attaches event listeners to existing DOM
// 4. React takes over and becomes interactive

// Hydration errors occur when:
// - Client and server render different HTML
// - Missing elements on client
// - Text content mismatch

// Avoid hydration errors:
function ClientOnly({ children }) {
  const [hasMounted, setHasMounted] = useState(false);
  
  useEffect(() => {
    setHasMounted(true);
  }, []);
  
  if (!hasMounted) {
    return null;
  }
  
  return children;
}

25. Explain render batching in React 18

jsx
// Before React 18 - Automatic batching only in React event handlers
function OldBatching() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    // These are batched in React 17 (single re-render)
    setCount(c => c + 1);
    setFlag(f => !f);
  }

  useEffect(() => {
    // These are NOT batched in React 17 (two re-renders)
    setCount(c => c + 1);
    setFlag(f => !f);
  }, []);

  return <button onClick={handleClick}>Click</button>;
}

// React 18 - Automatic batching everywhere
function NewBatching() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    // Batched (single re-render)
    setCount(c => c + 1);
    setFlag(f => !f);
  }

  useEffect(() => {
    // Also batched in React 18 (single re-render)
    setCount(c => c + 1);
    setFlag(f => !f);
  }, []);

  // Manual flush for edge cases
  function handleClickAsync() {
    setTimeout(() => {
      ReactDOM.flushSync(() => {
        setCount(c => c + 1);
      });
      // Not batched with above
      setFlag(f => !f);
    });
  }

  return <button onClick={handleClick}>Click</button>;
}

6. Concurrent Features <a name="concurrent-features"></a>

26. Implement useDeferredValue for deferring non-critical updates

jsx
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      <SearchSuggestions query={deferredQuery} />
    </div>
  );
}

// Practical example with search
function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  
  // Memoize to prevent unnecessary re-renders
  const suggestions = useMemo(() => {
    return computeSuggestions(deferredQuery);
  }, [deferredQuery]);

  return (
    <>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      
      {/* Show loading indicator when stale */}
      {query !== deferredQuery && <Spinner />}
      
      {/* Results update with lower priority */}
      <SearchResults results={suggestions} />
    </>
  );
}

27. How does useTransition work with Suspense?

jsx
function ProfilePage() {
  const [resource, setResource] = useState(initialResource);
  const [isPending, startTransition] = useTransition();

  function handleRefresh() {
    startTransition(() => {
      // This update has lower priority
      setResource(fetchProfileData());
    });
  }

  return (
    <div>
      <button 
        onClick={handleRefresh}
        disabled={isPending}
      >
        {isPending ? 'Refreshing...' : 'Refresh'}
      </button>
      
      <Suspense fallback={<Spinner />}>
        <ProfileDetails resource={resource} />
      </Suspense>
      
      <Suspense fallback={<Spinner />}>
        <ProfileTimeline resource={resource} />
      </Suspense>
    </div>
  );
}

// Nested Suspense with transitions
function NestedSuspense() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <>
      <TabButton
        isActive={tab === 'home'}
        onClick={() => selectTab('home')}
      >
        Home
      </TabButton>
      
      <Suspense fallback={<TabSpinner />}>
        {tab === 'home' ? <Home /> : <About />}
      </Suspense>
    </>
  );
}

28. Create a custom Suspense boundary with error handling

jsx
function SuspenseWithErrorBoundary({ 
  children, 
  fallback, 
  onError,
  errorComponent 
}) {
  const [error, setError] = useState(null);

  return (
    <ErrorBoundary
      onError={(err, errorInfo) => {
        setError(err);
        onError?.(err, errorInfo);
      }}
      resetKeys={[children]}
      fallback={errorComponent || <DefaultError />}
    >
      <React.Suspense fallback={fallback}>
        {error ? (
          <button onClick={() => setError(null)}>
            Retry
          </button>
        ) : (
          children
        )}
      </React.Suspense>
    </ErrorBoundary>
  );
}

// Usage with resource fetching
function UserProfile({ userId }) {
  const resource = userResourceCache.read(userId);
  
  return (
    <SuspenseWithErrorBoundary
      fallback={<ProfileSkeleton />}
      errorComponent={<ProfileError />}
    >
      <ProfileDetails resource={resource} />
    </SuspenseWithErrorBoundary>
  );
}

29. Implement startTransition for non-urgent navigation

jsx
function App() {
  const [location, setLocation] = useState(initialLocation);
  const [isNavigating, startNavigation] = useTransition();
  const match = useMatch(location);

  const navigate = useCallback((to) => {
    startNavigation(() => {
      setLocation(to);
    });
  }, [startNavigation]);

  return (
    <div className={isNavigating ? 'navigating' : ''}>
      <nav>
        <button onClick={() => navigate('/')}>Home</button>
        <button onClick={() => navigate('/about')}>About</button>
        <button onClick={() => navigate('/contact')}>Contact</button>
      </nav>
      
      {isNavigating && <NavigationIndicator />}
      
      <Suspense fallback={<PageSkeleton />}>
        {match === '/' && <Home />}
        {match === '/about' && <About />}
        {match === '/contact' && <Contact />}
      </Suspense>
    </div>
  );
}

30. Explain Concurrent Mode's interruptible rendering

jsx
// Without interruptible rendering
function BlockingComponent({ data }) {
  // If this takes a long time, it blocks the main thread
  const processedData = processLargeData(data);
  
  return <Result data={processedData} />;
}

// With interruptible rendering
function InterruptibleComponent({ data }) {
  const [processedData, setProcessedData] = useState(null);
  
  useEffect(() => {
    // Create a controller to abort if needed
    const controller = new AbortController();
    
    const processData = async () => {
      // This can be interrupted
      const result = await processLargeDataAsync(data, controller.signal);
      
      if (!controller.signal.aborted) {
        setProcessedData(result);
      }
    };
    
    processData();
    
    return () => {
      controller.abort();
    };
  }, [data]);
  
  if (!processedData) {
    return <Skeleton />;
  }
  
  return <Result data={processedData} />;
}

// Using useDeferredValue for interruptible rendering
function SearchWithInterruption({ query }) {
  const deferredQuery = useDeferredValue(query);
  
  // This component can be interrupted if new query comes
  const results = useMemo(() => {
    return searchAPI(deferredQuery);
  }, [deferredQuery]);
  
  return (
    <div>
      <SearchResults results={results} />
      {query !== deferredQuery && <LoadingIndicator />}
    </div>
  );
}

(Due to character limits, I'm including the remaining categories with key questions but shorter explanations. The full guide would continue in this pattern.)

7. Testing & Debugging <a name="testing-debugging"></a>

31. Test custom hooks with React Testing Library

32. Implement integration tests for React Query

33. Debug performance issues with React DevTools Profiler

34. Test error boundaries and error recovery

35. Mock WebSocket/Server Sent Events in tests

8. Architecture & Scalability <a name="architecture"></a>

36. Implement feature-based architecture

37. Create a plugin system for extensible components

38. Design a micro-frontend architecture with React

39. Implement module federation with Webpack 5

40. Create a design system with styled-components

9. TypeScript with React <a name="typescript"></a>

41. Advanced TypeScript utility types for React

42. Type-safe Redux with TypeScript

43. Generic components with TypeScript

44. Type-safe event handling

45. Discriminated unions for state management

10. Ecosystem & Tooling <a name="ecosystem"></a>

46. Optimize Webpack configuration for React

47. Implement PWA with Workbox and React

48. Set up monorepo with Turborepo

49. Implement CI/CD for React applications

50. Performance monitoring with RUM (Real User Monitoring)


Bonus: Advanced Patterns & 2026 Trends

51. React Server Components implementation

jsx
// Server Component (cannot use hooks, effects, state)
async function ProductDetails({ productId }) {
  const product = await db.products.find(productId);
  const reviews = await db.reviews.findByProduct(productId);
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      {/* Client Component nested inside */}
      <AddToCart productId={productId} />
      {/* Another Server Component */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews reviews={reviews} />
      </Suspense>
    </div>
  );
}

52. Islands Architecture with React

53. Edge rendering with React

54. WebAssembly integration with React

55. React Native Web for cross-platform


Preparation Strategy for 2026:

  1. Master Concurrent Features - These will be standard by 2026

  2. Learn Server Components - The future of React rendering

  3. Understand Build Tools - Turbopack, Rspack, Vite

  4. Practice Performance Patterns - Bundle analysis, code splitting, caching

  5. Study Real-world Architectures - Micro-frontends, monorepos, design systems

#career

Ready to Build Your Resume?

Create a professional resume that stands out to recruiters with our AI-powered builder.

50+ Advanced Mid-Level React Interview Questions (2026) | Hirecta Interview Prep | Hirecta