⚡ TECH BLOG
Home
Blog
Tags
About
⚡

Powered by Next.js 15 & Modern Web Tech ⚡

Back to Home

React 18: New Features and Concurrent Rendering

March 15, 2022
reactjavascriptweb-developmentfrontend
React 18: New Features and Concurrent Rendering

React 18: New Features and Concurrent Rendering

React 18 introduces concurrent features that fundamentally change how React renders your applications. These features enable more responsive user interfaces by allowing React to prepare multiple versions of the UI simultaneously.

Upgrading to React 18

npm install react react-dom

Enabling Concurrent Features

// Before (React 17)
import ReactDOM from 'react-dom'
ReactDOM.render(<App />, document.getElementById('root'))

// After (React 18)
import { createRoot } from 'react-dom/client'
const root = createRoot(document.getElementById('root'))
root.render(<App />)

Automatic Batching

React 18 automatically batches state updates, even in promises, setTimeout, and native event handlers.

Before React 18

// This would cause 2 re-renders in React 17
async function handleClick() {
  await fetchData()
  setData(data)      // Re-render 1
  setLoading(false) // Re-render 2
}

// Also in setTimeout
setTimeout(() => {
  setCount(1)        // Re-render 1
  setFlag(true)     // Re-render 2
}, 1000)

In React 18

// Now automatically batched - only 1 re-render!
async function handleClick() {
  await fetchData()
  setData(data)      // 
  setLoading(false) // Batched together
}

// Also works in setTimeout
setTimeout(() => {
  setCount(1)        // 
  setFlag(true)      // Batched together
}, 1000)

// And native events
element.addEventListener('click', () => {
  setCount(1)        // 
  setFlag(true)      // Only 1 re-render
})

Opting Out of Batching

Use flushSync when you need to force immediate updates:

import { flushSync } from 'react-dom'

function handleClick() {
  flushSync(() => {
    setCount(1) // Immediately re-renders
  })
  setFlag(true) // Another re-render
}

Transitions

Transitions let you mark certain updates as non-urgent, keeping the UI responsive during heavy operations.

useTransition Hook

import { useTransition, useState } from 'react'

function SearchComponent() {
  const [isPending, startTransition] = useTransition()
  const [searchTerm, setSearchTerm] = useState('')
  const [results, setResults] = useState([])

  const handleSearch = (e) => {
    const value = e.target.value
    
    // Urgent: Update input immediately
    setSearchTerm(value)
    
    // Non-urgent: Mark expensive operation as transition
    startTransition(() => {
      const filtered = largeDataArray.filter(item => 
        item.name.includes(value)
      )
      setResults(filtered)
    })
  }

  return (
    <div>
      <input 
        type="text" 
        value={searchTerm}
        onChange={handleSearch}
      />
      {isPending && <span>Loading...</span>}
      <Results items={results} />
    </div>
  )
}

useDeferredValue

useDeferredValue creates a deferred version of a value that lags behind updates:

import { useDeferredValue, useMemo, useState } from 'react'

function SearchResults({ query, data }) {
  // Deferred value lags behind query
  const deferredQuery = useDeferredValue(query)
  
  // Expensive computation runs against deferred value
  const filteredItems = useMemo(() => {
    return data.filter(item => 
      item.name.toLowerCase().includes(deferredQuery.toLowerCase())
    )
  }, [deferredQuery, data])

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}

Suspense for Data Fetching

React 18 enhances Suspense to work with data fetching, not just code splitting.

Basic Suspense Pattern

import { Suspense } from 'react'

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <DataComponent />
    </Suspense>
  )
}

Using Suspense with Data Fetching

// api.js - Using a simple Suspense-compatible resource
function fetchData(url) {
  let status = 'pending'
  let result
  let suspender = fetch(url)
    .then(r => r.json())
    .then(data => {
      status = 'success'
      result = data
    })
    .catch(error => {
      status = 'error'
      result = error
    })

  return {
    read() {
      if (status === 'pending') throw suspender
      if (status === 'error') throw result
      return result
    }
  }
}

// Component.js
const resource = fetchData('/api/users')

function Users() {
  const users = resource.read() // Suspends until data is ready
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

// App.js
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Users />
    </Suspense>
  )
}

Suspense with Transitions

import { Suspense, useState, useTransition } from 'react'

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

  const selectTab = (nextTab) => {
    startTransition(() => {
      setTab(nextTab)
    })
  }

  return (
    <div>
      <TabBar onSelect={selectTab} />
      {isPending && <Spinner />}
      <Suspense fallback={<Loading />}>
        <TabContent tab={tab} />
      </Suspense>
    </div>
  )
}

New Hooks

useId

Generate unique IDs for accessibility attributes:

import { useId } from 'react'

function FormField({ label }) {
  const id = useId()
  
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} type="text" />
    </div>
  )
}

// Multiple IDs
function Checkbox() {
  const id = useId()
  
  return (
    <>
      <label htmlFor={`${id}-checkbox`}>Accept terms</label>
      <input id={`${id}-checkbox`} type="checkbox" />
    </>
  )
}

useSyncExternalStore

Synchronize React state with external stores:

import { useSyncExternalStore } from 'react'

// Subscribe to browser APIs
function useOnlineStatus() {
  return useSyncExternalStore(
    // Subscribe function
    (callback) => {
      window.addEventListener('online', callback)
      window.addEventListener('offline', callback)
      return () => {
        window.removeEventListener('online', callback)
        window.removeEventListener('offline', callback)
      }
    },
    // Get snapshot
    () => navigator.onLine,
    // Server snapshot
    () => true
  )
}

function StatusIndicator() {
  const isOnline = useOnlineStatus()
  return <span>{isOnline ? 'Online' : 'Offline'}</span>
}

useInsertionEffect

For CSS-in-JS libraries to inject styles:

import { useInsertionEffect } from 'react'

function useStyle(rule) {
  useInsertionEffect(() => {
    const style = document.createElement('style')
    style.textContent = rule
    document.head.appendChild(style)
    return () => document.head.removeChild(style)
  }, [rule])
}

Streaming Server-Side Rendering

React 18 enables streaming HTML with selective hydration.

Using with Node.js Streams

// server.js
import { renderToPipeableStream } from 'react-dom/server'
import App from './App'

app.get('*', (req, res) => {
  const { pipe } = renderToPipeableStream(<App />, {
    bootstrapScripts: ['/client.js'],
    onShellReady() {
      res.setHeader('Content-Type', 'text/html')
      pipe(res)
    },
  })
})

Suspense in SSR

// App.js
function App() {
  return (
    <html>
      <head>
        <title>My App</title>
      </head>
      <body>
        <Navbar />
        <Suspense fallback={<ArticleSkeleton />}>
          <Article />
        </Suspense>
        <Footer />
      </body>
    </html>
  )
}

Strict Mode Changes

React 18's Strict Mode simulates mounting, unmounting, and remounting components to help find side effects:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
)

Handling Double Effects

function Component() {
  useEffect(() => {
    // This runs twice in Strict Mode
    console.log('Mount')
    
    return () => {
      console.log('Unmount')
    }
  }, [])

  // Fix: Clean up properly
  useEffect(() => {
    const controller = new AbortController()
    
    fetchData({ signal: controller.signal })
    
    return () => {
      controller.abort()
    }
  }, [])
}

Performance Best Practices

1. Use Transitions for Expensive Updates

function List({ items }) {
  const [filter, setFilter] = useState('')
  const [isPending, startTransition] = useTransition()

  const handleFilter = (value) => {
    // Immediate UI update
    setFilter(value)
    
    // Defer expensive filtering
    startTransition(() => {
      setFilteredItems(filterItems(items, value))
    })
  }

  return (
    <div>
      <input onChange={(e) => handleFilter(e.target.value)} />
      {isPending ? <Loading /> : <Items items={filteredItems} />}
    </div>
  )
}

2. Memoize Deferred Computations

import { useDeferredValue, useMemo } from 'react'

function Search({ query, items }) {
  const deferredQuery = useDeferredValue(query)
  
  const results = useMemo(() => {
    return heavySearch(items, deferredQuery)
  }, [deferredQuery, items])

  return <Results results={results} />
}

3. Use Suspense Boundaries Strategically

function App() {
  return (
    <Layout>
      <Suspense fallback={<NavSkeleton />}>
        <Navigation />
      </Suspense>
      
      <main>
        <Suspense fallback={<ContentSkeleton />}>
          <Content />
        </Suspense>
      </main>
      
      <Suspense fallback={<FooterSkeleton />}>
        <Footer />
      </Suspense>
    </Layout>
  )
}

Conclusion

React 18's concurrent features represent a significant evolution in how React handles rendering. By leveraging automatic batching, transitions, and Suspense, you can build more responsive and user-friendly applications.

Migration Checklist

  • Upgrade to React 18 and update render to createRoot
  • Review effects for proper cleanup (Strict Mode tests this)
  • Identify opportunities for useTransition in slow interactions
  • Consider useDeferredValue for type-ahead scenarios
  • Use useId for stable unique IDs

React 18 opens up exciting possibilities for building faster, more responsive user interfaces. Start exploring these features today!

Share:

💬 Comments