Stable React Rendering

Write React components and tests so they don’t depend on React’s internal render/callback sequencing, and so Suspense boundaries capture the work that actually suspends.

copy reviewer prompt

Prompt

Reviewer Prompt

Write React components and tests so they don’t depend on React’s internal render/callback sequencing, and so Suspense boundaries capture the work that actually suspends.

Apply:

  • Put the suspending query/resource inside the component that’s rendered within the Suspense boundary (move query hooks into a child component).
  • Don’t use boolean “first/last render” assumptions or logic tied to how many times React calls callbacks; React 19 can change counts/order.
  • Base tests on observable state/DOM output; if React 18 vs 19 truly differs, branch expectations by version.
  • Prefer effects for client-only orchestration/updates; use lazy initializers for useState when constructing objects.

Example (Suspense + boundary correctness):

import { createQuery } from '@tanstack/solid-query' // or equivalent React Query patterns
import React, { Suspense } from 'react'

function Posts({ postId }: { postId: number }) {
  const query = createQuery(() => ({
    queryKey: ['posts', postId],
    queryFn: () => fetch('/api/posts/' + postId).then(r => r.json()),
  }))

  return <div>{String(query.data?.title ?? '')}</div>
}

export function Home() {
  const [postId, setPostId] = React.useState(1)

  return (
    <>
      <button onClick={() => setPostId(id => Math.max(1, id - 1))}>Prev</button>
      <button onClick={() => setPostId(id => id + 1)}>Next</button>

      <Suspense fallback={<div>Loading post...</div>}>
        <Posts postId={postId} />
      </Suspense>
    </>
  )
}

Example (avoid render-count assumptions):

// Instead of: if (!boundary) { ... }
// Use count/state-based expectations or version-conditional assertions.
let callCount = 0
const useErrorBoundary = () => (++callCount, callCount < 3)

Source discussions