DEV Community

Cover image for React Concurrent Mode Isn't Magic - It's Just Really Smart Priorities (And This Changes Everything)
Mohamad Msalme
Mohamad Msalme

Posted on

React Concurrent Mode Isn't Magic - It's Just Really Smart Priorities (And This Changes Everything)

Know WHY β€” Let AI Handle the HOW πŸ€–

Ever wonder why your React app feels sluggish when you type in a search box? Or why clicking a tab freezes the UI for a second? You're probably thinking "I need to optimize my components" or "I should debounce everything."

What if I told you the real problem isn't your code - it's that React is treating all updates with the same urgency, and there's a simple mental model shift that makes everything feel instantly responsive?

πŸ€” Let's Start With The Problem Everyone Has

You're building a search feature. Simple, right?

function SearchPage() {
  const [searchTerm, setSearchTerm] = useState('');

  // Heavy filtering of 10,000 items
  const results = useMemo(() => {
    return bigDataset.filter(item => 
      item.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }, [searchTerm]);

  return (
    <div>
      <input 
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
      />
      <ResultsList items={results} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Problem: You type "react" and the input feels laggy. Each keystroke triggers expensive filtering, blocking the UI.

You think: "I should debounce this!"

But what if there's a better way that doesn't require manually managing timers?

🧠 Think Like a Hospital Emergency Room for a Moment

Imagine you're running an ER. Two patients arrive:

  • Patient A: Broken finger (painful but not urgent)
  • Patient B: Heart attack (URGENT!)

You don't treat them in order of arrival. You prioritize based on urgency.

That's exactly what React Concurrent Mode does with updates.

Before Concurrent Mode, React was like treating patients strictly in arrival order - no matter how urgent. With Concurrent Mode, React can:

  • Recognize which updates are urgent (user typing)
  • Which updates can wait (expensive filtering)
  • Interrupt low-priority work if something urgent arrives

πŸ”‘ The Mental Model That Changes Everything

Here's the insight most developers miss:

React doesn't magically detect "slow" components. YOU tell React which updates can be deferred.

import { useDeferredValue } from 'react';

function SearchPage() {
  const [searchTerm, setSearchTerm] = useState('');
  const deferredSearchTerm = useDeferredValue(searchTerm);

  const results = useMemo(() => {
    console.log('Filtering for:', deferredSearchTerm);
    return bigDataset.filter(item => 
      item.name.toLowerCase().includes(deferredSearchTerm.toLowerCase())
    );
  }, [deferredSearchTerm]);

  return (
    <div>
      <input 
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
      />
      <p>Searching for: {searchTerm}</p>
      <ResultsList items={results} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What just happened?

  1. searchTerm updates immediately when you type (HIGH PRIORITY)
  2. Input shows new character instantly - UI feels responsive!
  3. deferredSearchTerm "lags behind" - updates later (LOW PRIORITY)
  4. Expensive filtering uses the deferred value
  5. React can interrupt filtering if you keep typing

Console output when you type "react" quickly:

// Without useDeferredValue:
Filtering for: r
Filtering for: re  
Filtering for: rea
Filtering for: reac
Filtering for: react
// 5 expensive operations! UI lags on every keystroke 😑

// With useDeferredValue:
Filtering for: react
// Only 1 operation after you stop typing! βœ…
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ The Critical Insight: It's About the VALUE, Not the Component

This is the part that blew my mind:

function DisplayCounter({ count }) {
  return <div>Count: {count}</div>;
}

function App() {
  const [count, setCount] = useState(0);
  const deferredCount = useDeferredValue(count);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>

      {/* Same component, different priorities! */}
      <DisplayCounter count={count} />         {/* HIGH PRIORITY */}
      <DisplayCounter count={deferredCount} /> {/* LOW PRIORITY */}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Same component rendered twice, but:

  • First instance uses count β†’ renders immediately
  • Second instance uses deferredCount β†’ renders later

React assigns priority based on which value the component receives, not the component itself!

🎯 Real-World Example: Auto-Complete Search

Let's see the difference in user experience:

function AutoComplete() {
  const [input, setInput] = useState('');
  const deferredInput = useDeferredValue(input);

  // Expensive: searches through 100,000 items
  const suggestions = useMemo(() => {
    return searchDatabase(deferredInput);
  }, [deferredInput]);

  return (
    <div>
      <input
        value={input}
        onChange={e => setInput(e.target.value)}
        placeholder="Type to search..."
      />

      {/* Always shows fresh input */}
      <SearchStatus query={input} />

      {/* Shows suggestions for deferred input */}
      <SuggestionsList items={suggestions} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Timeline when you type "typescript":

User types: t β†’ ty β†’ typ β†’ type β†’ types β†’ typesc β†’ typescr β†’ typescri β†’ typescript

Without useDeferredValue:
0ms:   Type "t"
1ms:   input = "t"
2ms:   Start expensive search for "t"
       ... UI FREEZES 50ms ...
52ms:  Show results for "t"
53ms:  Type "y" (but you had to wait!)
       ... 9 expensive searches total! 😑

With useDeferredValue:
0ms:   Type "t"  
1ms:   input = "t" βœ…
2ms:   Input shows "t" immediately!
3ms:   deferredInput still = ""
4ms:   Start search for "t" (low priority)
10ms:  Type "y"
11ms:  input = "ty" βœ…
12ms:  Input shows "ty" immediately!
13ms:  React CANCELS search for "t"
14ms:  Start NEW search for "ty"
... you keep typing fast ...
500ms: You stop at "typescript"
600ms: deferredInput finally = "typescript"
650ms: ONE search completes
       ... Only 1 expensive search! βœ…
Enter fullscreen mode Exit fullscreen mode

Your input stays responsive because React prioritizes showing your keystrokes over computing search results.

⚑ useDeferredValue vs useTransition: When to Use What?

Both enable concurrent rendering, but for different scenarios:

useDeferredValue - Defer Expensive Renders

Use when: You have expensive computations during render that you can't control.

function DataDashboard() {
  const [filter, setFilter] = useState('all');
  const deferredFilter = useDeferredValue(filter);

  // Expensive computation during render
  const processedData = useMemo(() => {
    return heavyDataProcessing(deferredFilter);
  }, [deferredFilter]);

  return (
    <div>
      <FilterButtons current={filter} onChange={setFilter} />
      <DataTable data={processedData} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

useTransition - Control State Updates

Use when: You control the state update and want to mark it as non-urgent.

function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  const switchTab = (newTab) => {
    startTransition(() => {
      setTab(newTab);
    });
  };

  return (
    <div>
      <button onClick={() => switchTab('profile')}>
        Profile {isPending && '⏳'}
      </button>

      {tab === 'home' && <HomeTab />}
      {tab === 'profile' && <ProfileTab />}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The difference:

  • useDeferredValue: "This value can lag behind"
  • useTransition: "This state update isn't urgent"

🏠 The Perfect Analogy: Email Priority Flags

Think of your React updates like emails:

const urgentUpdate = useState(value);        // πŸ”΄ HIGH PRIORITY
const normalUpdate = useDeferredValue(value); // 🟑 LOW PRIORITY

// Components using urgentUpdate:
// β†’ Process immediately (like urgent emails)

// Components using normalUpdate:  
// β†’ Process when you have time (like regular emails)
Enter fullscreen mode Exit fullscreen mode

When your inbox gets flooded:

  • Urgent emails (user input) get handled first
  • Regular emails (expensive renders) can wait
  • You don't freeze waiting to process every email in order

That's concurrent rendering!

πŸ’­ Common Misconception: "Won't This Show Stale Data?"

You might think: "If deferredValue lags behind, won't users see outdated results?"

Yes, briefly - and that's actually GOOD UX!

function SearchResults() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  return (
    <div>
      {/* User always sees what they typed */}
      <input value={query} onChange={e => setQuery(e.target.value)} />

      {/* Visual feedback when results are updating */}
      {query !== deferredQuery && <Spinner />}

      {/* Results for deferred query */}
      <Results query={deferredQuery} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Users prefer:

  • βœ… Responsive input + brief spinner
  • ❌ Laggy input that freezes on every keystroke

🎯 Real-World Pattern: Dashboard with Heavy Charts

function Dashboard() {
  const [metric, setMetric] = useState('revenue');
  const deferredMetric = useDeferredValue(metric);

  return (
    <div>
      {/* Tabs respond instantly */}
      <Tabs selected={metric} onChange={setMetric} />

      {/* Visual feedback */}
      {metric !== deferredMetric && <ChartSkeleton />}

      {/* Heavy chart renders with low priority */}
      <ExpensiveChart metric={deferredMetric} />
    </div>
  );
}

function ExpensiveChart({ metric }) {
  // Heavy computation during render
  const chartData = useMemo(() => {
    const data = [];
    for (let i = 0; i < 10000; i++) {
      data.push(calculateComplexDataPoint(metric, i));
    }
    return data;
  }, [metric]);

  return <ChartLibrary data={chartData} />;
}
Enter fullscreen mode Exit fullscreen mode

User Experience:

User clicks "Profit" tab:

Without useDeferredValue:
0ms:   Click "Profit"
1ms:   metric = "profit"
2ms:   Start rendering ExpensiveChart
       ... UI COMPLETELY FROZEN for 800ms ...
800ms: Chart finally renders
       User frustrated, thinks app crashed 😑

With useDeferredValue:
0ms:   Click "Profit"
1ms:   metric = "profit"
2ms:   Tab switches to "Profit" immediately βœ…
3ms:   User sees visual feedback (skeleton)
4ms:   deferredMetric still = "revenue"
5ms:   Start rendering new chart in background
       ... UI STAYS RESPONSIVE ...
800ms: Chart smoothly appears
       User happy, knows their click worked! 😊
Enter fullscreen mode Exit fullscreen mode

🧠 The Mental Model Shift

Stop Thinking:

  • "I need to debounce everything"
  • "My components are too slow"
  • "I should use setTimeout to avoid blocking"

Start Thinking:

  • "Which updates need immediate feedback?"
  • "Which expensive work can wait?"
  • "Let React prioritize based on urgency"

πŸ’‘ When to Use Concurrent Features

Use useDeferredValue when:

  • βœ… Heavy filtering/searching while typing
  • βœ… Expensive computations during render
  • βœ… Large list rendering that lags input
  • βœ… Data visualization that blocks UI

Don't use it when:

  • ❌ Component is already fast
  • ❌ You need synchronous updates (form validation)
  • ❌ The "lag" would confuse users (critical UI feedback)

🎯 The Takeaway

Many developers learn the HOW: "Use useDeferredValue to defer values."

When you understand the WHY: "React prioritizes updates based on urgency, and you control priorities by choosing which values to defer," you gain a powerful tool that makes your UI feel instantly responsive without manual optimization.

Top comments (0)