Modern React 18+ Features REACT 18+

1. useTransition Hook for Non-blocking Updates NEW

Feature Syntax Description Use Case
useTransition Hook const [isPending, startTransition] = useTransition() Mark state updates as non-urgent transitions Keep UI responsive during updates
startTransition startTransition(() => {}) Wrap state updates to mark as transition Heavy computations, filtering
isPending Flag isPending ? 'Loading' : 'Done' Boolean indicating transition in progress Show loading indicators
Priority System Urgent vs transition updates React prioritizes urgent updates first Input responsiveness
Interruptible Renders React can pause transitions Allow urgent updates to interrupt Smooth user experience

Example: useTransition for responsive filtering

import { useState, useTransition } from 'react';

const SearchableList = ({ items }) => {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();
  
  const handleSearch = (value) => {
    // Urgent: Update input immediately
    setQuery(value);
    
    // Non-urgent: Filter in background
    startTransition(() => {
      const filtered = items.filter(item =>
        item.name.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredItems(filtered);
    });
  };
  
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="Search..."
      />
      
      {isPending ? (
        <div>Updating results...</div>
      ) : (
        <ul>
          {filteredItems.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
};

// Tab switching example
const TabContainer = () => {
  const [activeTab, setActiveTab] = useState('tab1');
  const [isPending, startTransition] = useTransition();
  
  const switchTab = (tab) => {
    startTransition(() => {
      setActiveTab(tab);
    });
  };
  
  return (
    <div>
      <div className={isPending ? 'loading' : ''}>
        <button onClick={() => switchTab('tab1')}>Tab 1</button>
        <button onClick={() => switchTab('tab2')}>Tab 2</button>
        <button onClick={() => switchTab('tab3')}>Tab 3</button>
      </div>
      
      <TabContent tab={activeTab} />
    </div>
  );
};
Note: useTransition enables concurrent rendering. Transition updates can be interrupted by more urgent updates, keeping the UI responsive even during expensive operations.

2. useDeferredValue Hook for Deferred Values NEW

Feature Syntax Description Use Case
useDeferredValue const deferred = useDeferredValue(value) Defer updating a value to keep UI responsive Expensive child component updates
Deferred Rendering <Component value={deferred} /> Pass deferred value to expensive component Debounce-like behavior
Initial Value useDeferredValue(value, initialValue) Optional initial value during SSR Server-side rendering
Value Comparison value !== deferred Check if deferral is in progress Show loading states
import { useState, useDeferredValue, memo } from 'react';

const SearchResults = ({ query }) => {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  
  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      <ExpensiveList query={deferredQuery} />
    </div>
  );
};

// Expensive component that benefits from deferral
const ExpensiveList = memo(({ query }) => {
  const items = useMemo(() => {
    // Expensive filtering/computation
    return largeDataset.filter(item =>
      item.name.toLowerCase().includes(query.toLowerCase())
    );
  }, [query]);
  
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});

// Complete search component
const SearchPage = () => {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const isSearching = query !== deferredQuery;
  
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      
      {isSearching && <span>Searching...</span>}
      
      <SearchResults query={deferredQuery} />
    </div>
  );
};

// Comparison: useTransition vs useDeferredValue
// useTransition: You control the state update
// useDeferredValue: React controls when to update the value
Note: Use useDeferredValue when you can't control the state update (props from parent). Use useTransition when you control the state update.

3. useId Hook for Unique Identifiers NEW

Feature Syntax Description Use Case
useId Hook const id = useId() Generate unique ID for accessibility attributes Form labels, ARIA attributes
SSR Safe Same ID on client and server Consistent IDs across hydration Avoid hydration mismatches
Multiple IDs id + '-label', id + '-error' Derive multiple IDs from single hook Related elements
List Items Don't use for key prop Keys should be from data, not generated Use stable data IDs

Example: useId for accessible form fields

import { useId } from 'react';

// Basic form field with unique ID
const TextField = ({ label, error }) => {
  const id = useId();
  const errorId = id + '-error';
  
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        type="text"
        aria-describedby={error ? errorId : undefined}
        aria-invalid={error ? 'true' : 'false'}
      />
      {error && (
        <span id={errorId} role="alert">
          {error}
        </span>
      )}
    </div>
  );
};

// Complex form with multiple fields
const ContactForm = () => {
  const id = useId();
  
  return (
    <form>
      <div>
        <label htmlFor={id + '-name'}>Name</label>
        <input id={id + '-name'} type="text" />
      </div>
      
      <div>
        <label htmlFor={id + '-email'}>Email</label>
        <input id={id + '-email'} type="email" />
      </div>
      
      <div>
        <label htmlFor={id + '-message'}>Message</label>
        <textarea id={id + '-message'} />
      </div>
    </form>
  );
};

// Reusable input component
const Input = ({ label, type = 'text', ...props }) => {
  const id = useId();
  
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} type={type} {...props} />
    </>
  );
};

// ARIA example
const Accordion = ({ title, children }) => {
  const [isOpen, setIsOpen] = useState(false);
  const id = useId();
  const headerId = id + '-header';
  const panelId = id + '-panel';
  
  return (
    <div>
      <button
        id={headerId}
        aria-expanded={isOpen}
        aria-controls={panelId}
        onClick={() => setIsOpen(!isOpen)}
      >
        {title}
      </button>
      
      <div
        id={panelId}
        role="region"
        aria-labelledby={headerId}
        hidden={!isOpen}
      >
        {children}
      </div>
    </div>
  );
};
Warning: Don't use useId to generate keys in a list. Keys should come from your data. Use useId only for accessibility attributes like id, htmlFor, aria-*.

4. useSyncExternalStore Hook for External State NEW

Feature Syntax Description Use Case
useSyncExternalStore useSyncExternalStore(subscribe, getSnapshot) Subscribe to external store Redux, Zustand, browser APIs
Subscribe Function subscribe(callback) Subscribe to store changes Return unsubscribe function
getSnapshot Function getSnapshot() Get current store value Must return immutable value
SSR getSnapshot useSyncExternalStore(..., getServerSnapshot) Server-side initial value Hydration consistency
Tearing Prevention Consistent state across components No visual inconsistencies Concurrent rendering safety

Example: useSyncExternalStore for browser APIs

import { useSyncExternalStore } from 'react';

// Browser online status
const useOnlineStatus = () => {
  return useSyncExternalStore(
    // Subscribe function
    (callback) => {
      window.addEventListener('online', callback);
      window.addEventListener('offline', callback);
      
      return () => {
        window.removeEventListener('online', callback);
        window.removeEventListener('offline', callback);
      };
    },
    // getSnapshot function
    () => navigator.onLine,
    // getServerSnapshot (SSR)
    () => true
  );
};

// Usage
const StatusIndicator = () => {
  const isOnline = useOnlineStatus();
  
  return (
    <div>
      {isOnline ? '🟢 Online' : '🔴 Offline'}
    </div>
  );
};

// Window width hook
const useWindowWidth = () => {
  return useSyncExternalStore(
    (callback) => {
      window.addEventListener('resize', callback);
      return () => window.removeEventListener('resize', callback);
    },
    () => window.innerWidth,
    () => 0 // SSR default
  );
};

// Custom store example
class CountStore {
  constructor() {
    this.count = 0;
    this.listeners = new Set();
  }
  
  subscribe = (callback) => {
    this.listeners.add(callback);
    return () => this.listeners.delete(callback);
  };
  
  getSnapshot = () => {
    return this.count;
  };
  
  increment = () => {
    this.count++;
    this.listeners.forEach(listener => listener());
  };
}

const store = new CountStore();

const useCountStore = () => {
  return useSyncExternalStore(
    store.subscribe,
    store.getSnapshot
  );
};

const Counter = () => {
  const count = useCountStore();
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => store.increment()}>+</button>
    </div>
  );
};

// Local storage sync
const useLocalStorage = (key, initialValue) => {
  return useSyncExternalStore(
    (callback) => {
      const handler = (e) => {
        if (e.key === key) callback();
      };
      window.addEventListener('storage', handler);
      return () => window.removeEventListener('storage', handler);
    },
    () => {
      try {
        return JSON.parse(localStorage.getItem(key)) ?? initialValue;
      } catch {
        return initialValue;
      }
    },
    () => initialValue
  );
};
Note: useSyncExternalStore is primarily for library authors. Most apps should use built-in hooks or state management libraries that already use this hook internally.

5. useInsertionEffect Hook for CSS-in-JS Libraries NEW

Feature Syntax Description Use Case
useInsertionEffect useInsertionEffect(() => {}, [deps]) Insert styles before layout effects CSS-in-JS libraries only
Timing Before useLayoutEffect Runs before DOM mutations are visible Inject <style> tags
No DOM Access Cannot read/write DOM DOM not yet updated Style injection only
Library Use Only Not for application code Advanced library feature styled-components, emotion

Example: useInsertionEffect for style injection

import { useInsertionEffect } from 'react';

// CSS-in-JS library example (simplified)
const styleCache = new Map();

const useStyleInjection = (css, id) => {
  useInsertionEffect(() => {
    // Check if style already injected
    if (styleCache.has(id)) {
      return;
    }
    
    // Create and inject style tag
    const style = document.createElement('style');
    style.textContent = css;
    style.setAttribute('data-style-id', id);
    document.head.appendChild(style);
    
    // Cache it
    styleCache.set(id, style);
    
    // Cleanup
    return () => {
      document.head.removeChild(style);
      styleCache.delete(id);
    };
  }, [css, id]);
};

// Example usage in a CSS-in-JS library
const useStyledComponent = (styles) => {
  const id = useId();
  const className = \`styled-\${id}\`;
  
  const css = \`.styled-\${id} {
    \${Object.entries(styles)
      .map(([key, value]) => \`\${key}: \${value};\`)
      .join('\n')}
  }\`;
  
  useStyleInjection(css, id);
  
  return className;
};

// Usage in components
const MyComponent = () => {
  const className = useStyledComponent({
    'background-color': '#f0f0f0',
    'padding': '20px',
    'border-radius': '8px'
  });
  
  return <div className={className}>Styled Content</div>;
};

// Real-world pattern (styled-components-like)
const styled = (tag) => (styles) => {
  return (props) => {
    const id = useId();
    const className = \`sc-\${id}\`;
    
    useInsertionEffect(() => {
      const css = generateCSS(className, styles, props);
      injectStyle(css, id);
      
      return () => removeStyle(id);
    }, [className, styles, props]);
    
    return React.createElement(tag, {
      ...props,
      className: \`\${className} \${props.className || ''}\`
    });
  };
};

// Usage
const Button = styled('button')\`
  background: blue;
  color: white;
  padding: 10px 20px;
\`;

// In component
<Button>Click Me</Button>
Warning: useInsertionEffect is ONLY for CSS-in-JS library authors. Regular applications should never use this hook. Use useEffect or useLayoutEffect instead.

6. Concurrent Features and Automatic Batching NEW

Feature Description Benefit Example
Automatic Batching Multiple state updates batched automatically Fewer renders, better performance Event handlers, timeouts, promises
Concurrent Rendering React can interrupt rendering work Keep UI responsive during updates Large list updates
Transitions Mark updates as non-urgent Prioritize urgent updates useTransition, startTransition
Streaming SSR Send HTML in chunks as ready Faster Time to First Byte Suspense on server
Selective Hydration Hydrate components as user interacts Faster Time to Interactive Priority-based hydration
Strict Mode Double Render Effects run twice in development Catch side effect bugs early Development only

Example: Automatic batching in React 18

import { useState } from 'react';
import { flushSync } from 'react-dom';

// React 18: Automatic batching everywhere
const AutoBatchingExample = () => {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  
  console.log('Render'); // Only logs once per click
  
  // Event handlers - batched (React 17 too)
  const handleClick = () => {
    setCount(c => c + 1);
    setFlag(f => !f);
    // Only 1 render
  };
  
  // Timeouts - NOW batched (was 2 renders in React 17)
  const handleTimeout = () => {
    setTimeout(() => {
      setCount(c => c + 1);
      setFlag(f => !f);
      // Only 1 render in React 18!
    }, 1000);
  };
  
  // Promises - NOW batched (was 2 renders in React 17)
  const handleAsync = async () => {
    const data = await fetchData();
    setCount(data.count);
    setFlag(data.flag);
    // Only 1 render in React 18!
  };
  
  // Native event handlers - NOW batched
  useEffect(() => {
    const handler = () => {
      setCount(c => c + 1);
      setFlag(f => !f);
      // Only 1 render in React 18!
    };
    
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);
  
  // Opt-out of batching with flushSync (rare)
  const handleNoFlush = () => {
    flushSync(() => {
      setCount(c => c + 1);
    });
    // React renders here
    flushSync(() => {
      setFlag(f => !f);
    });
    // React renders again - 2 total renders
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Flag: {flag.toString()}</p>
      <button onClick={handleClick}>Update</button>
      <button onClick={handleTimeout}>Update (Timeout)</button>
      <button onClick={handleAsync}>Update (Async)</button>
    </div>
  );
};

// Concurrent features example
const ConcurrentExample = () => {
  const [resource, setResource] = useState(initialResource);
  
  // Wrap in transition for smooth updates
  const handleUpdate = () => {
    startTransition(() => {
      setResource(fetchNewResource());
    });
  };
  
  return (
    <Suspense fallback={<Spinner />}>
      <button onClick={handleUpdate}>Update</button>
      <ResourceComponent resource={resource} />
    </Suspense>
  );
};

// Strict Mode double render
const StrictModeExample = () => {
  useEffect(() => {
    console.log('Effect');
    // In development, logs twice to help find bugs
    
    return () => {
      console.log('Cleanup');
      // Also runs twice in development
    };
  }, []);
  
  return <div>Component</div>;
};

React 18 Key Improvements

  • Automatic Batching: Multiple state updates batched everywhere (promises, timeouts, native events)
  • Concurrent Rendering: React can pause and resume work, keeping UI responsive
  • useTransition: Mark updates as non-urgent for better UX
  • useDeferredValue: Defer expensive updates while keeping input responsive
  • Streaming SSR: Send HTML in chunks for faster initial load
  • Selective Hydration: Prioritize hydration based on user interaction
Note: React 18's concurrent features are opt-in. Use createRoot instead of render to enable them: ReactDOM.createRoot(container).render(<App />)