
React19 Concurrent Patterns
- 913 installs
- 37.1k repo stars
- Updated July 28, 2026
- github/awesome-copilot
react19-concurrent-patterns is an agent skill that preserves React 18 concurrent patterns and adopts React 19 APIs during framework migration.
About
The react19-concurrent-patterns skill covers two concerns during React 19 migration: preserving existing React 18 concurrent patterns and adopting new React 19 APIs after stabilization. Part one documents patterns that must survive migration unchanged, including createRoot, useTransition, useDeferredValue, and Suspense with React.lazy code splitting. Part two introduces React 19 APIs such as the use() hook for promises and context, Actions with useActionState and useFormStatus, useOptimistic, and Suspense for data fetching, with reference files for each. Migration safety rules require leaving concurrent-mode logic untouched during the migration itself and verifying via grep that useTransition, useDeferredValue, Suspense, and startTransition were not modified. New React 19 APIs should be adopted in a post-migration cleanup sprint, not during the core migration pass.
- Preserves React 18 useTransition, useDeferredValue, and Suspense patterns unchanged.
- Documents createRoot setup that carries forward from React 18 to React 19.
- Introduces React 19 use(), Actions, useOptimistic after migration stabilizes.
- Includes grep verification to confirm concurrent logic was not touched.
- References split into use(), Actions, and Suspense data-fetching guides.
React19 Concurrent Patterns by the numbers
- 913 all-time installs (skills.sh)
- +24 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #401 of 2,277 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
react19-concurrent-patterns capabilities & compatibility
- Capabilities
- react 18 concurrent pattern preservation · react 19 api adoption guidance · migration safety grep checks · use() and actions reference routing
- Use cases
- frontend · refactoring
npx skills add https://github.com/github/awesome-copilot --skill react19-concurrent-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 913 |
|---|---|
| repo stars | ★ 37.1k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | github/awesome-copilot ↗ |
How do I migrate to React 19 without breaking useTransition, Suspense, or other concurrent patterns from React 18?
Preserve React 18 concurrent patterns and adopt React 19 APIs like useTransition, useDeferredValue, Suspense, use(), useOptimistic, and Actions during migration.
Who is it for?
Developers migrating React 18 codebases to React 19 who need concurrent pattern preservation checklists.
Skip if: Skip when you only need source-level React 19 API rewrites without concurrent mode concerns.
When should I use this skill?
User asks about React 19 concurrent patterns, useTransition migration safety, or adopting use() and Actions.
What you get
A migration plan that keeps React 18 concurrent APIs intact and introduces React 19 hooks in a controlled post-migration sprint.
- Action handlers
- useActionState form components
Files
React 19 Concurrent Patterns
React 19 introduced new APIs that complement the migration work. This skill covers two concerns:
1. Preserve existing React 18 concurrent patterns that must not be broken during migration 2. Adopt new React 19 APIs worth introducing after migration stabilizes
Part 1 Preserve: React 18 Concurrent Patterns That Must Survive the Migration
These patterns exist in React 18 codebases and must not be accidentally removed or broken:
createRoot Already Migrated by the R18 Orchestra
If the R18 orchestra already ran, ReactDOM.render → createRoot is done. Verify it's correct:
// CORRECT React 19 root (same as React 18):
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);useTransition No Migration Needed
useTransition from React 18 works identically in React 19. Do not touch these patterns during migration:
// React 18 useTransition unchanged in React 19:
const [isPending, startTransition] = useTransition();
function handleClick() {
startTransition(() => {
setFilteredResults(computeExpensiveFilter(input));
});
}useDeferredValue No Migration Needed
// React 18 useDeferredValue unchanged in React 19:
const deferredQuery = useDeferredValue(query);Suspense for Code Splitting No Migration Needed
// React 18 Suspense with lazy unchanged in React 19:
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>
);
}---
Part 2 React 19 New APIs
These are worth adopting in a post-migration cleanup sprint. Do not introduce these DURING the migration stabilize first.
For full patterns on each new API, read:
- `references/react19-use.md` the
use()hook for promises and context - `references/react19-actions.md` Actions, useActionState, useFormStatus, useOptimistic
- `references/react19-suspense.md` Suspense for data fetching (the new pattern)
Migration Safety Rules
During the React 19 migration itself, these concurrent-mode patterns must be left completely untouched:
# Verify nothing touched these during migration:
grep -rn "useTransition\|useDeferredValue\|Suspense\|startTransition" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."If the migrator touched any of these files, review the changes the migration should only have modified React API surface (forwardRef, defaultProps, etc.), never concurrent mode logic.
React 19 Actions Pattern Reference
React 19 introduces Actions a pattern for handling async operations (like form submissions) with built-in loading states, error handling, and optimistic updates. This replaces the useReducer + state pattern with a simpler API.
What are Actions?
An Action is an async function that:
- Can be called automatically when a form submits or button clicks
- Runs with automatic loading/pending state
- Updates the UI automatically when done
- Works with Server Components for direct server mutation
---
useActionState()
useActionState is the client-side Action hook. It replaces useReducer + useEffect for form handling.
React 18 Pattern
// React 18 form with useReducer + state:
function Form() {
const [state, dispatch] = useReducer(
(state, action) => {
switch (action.type) {
case 'loading':
return { ...state, loading: true, error: null };
case 'success':
return { ...state, loading: false, data: action.data };
case 'error':
return { ...state, loading: false, error: action.error };
}
},
{ loading: false, data: null, error: null }
);
async function handleSubmit(e) {
e.preventDefault();
dispatch({ type: 'loading' });
try {
const result = await submitForm(new FormData(e.target));
dispatch({ type: 'success', data: result });
} catch (err) {
dispatch({ type: 'error', error: err.message });
}
}
return (
<form onSubmit={handleSubmit}>
<input name="email" />
{state.loading && <Spinner />}
{state.error && <Error msg={state.error} />}
{state.data && <Success data={state.data} />}
<button disabled={state.loading}>Submit</button>
</form>
);
}React 19 useActionState() Pattern
// React 19 same form with useActionState:
import { useActionState } from 'react';
async function submitFormAction(prevState, formData) {
// prevState = previous return value from this function
// formData = FormData from <form action={submitFormAction}>
try {
const result = await submitForm(formData);
return { data: result, error: null };
} catch (err) {
return { data: null, error: err.message };
}
}
function Form() {
const [state, formAction, isPending] = useActionState(
submitFormAction,
{ data: null, error: null } // initial state
);
return (
<form action={formAction}>
<input name="email" />
{isPending && <Spinner />}
{state.error && <Error msg={state.error} />}
{state.data && <Success data={state.data} />}
<button disabled={isPending}>Submit</button>
</form>
);
}Differences:
- One hook instead of
useReducer+ logic formActionreplacesonSubmit, form automatically collects FormDataisPendingis a boolean, no dispatch calls- Action function receives
(prevState, formData)
---
useFormStatus()
useFormStatus is a child component hook that reads the pending state from the nearest form. It acts like a built-in isPending signal without prop drilling.
// React 18 must pass isPending as prop:
function SubmitButton({ isPending }) {
return <button disabled={isPending}>Submit</button>;
}
function Form({ isPending, formAction }) {
return (
<form action={formAction}>
<input />
<SubmitButton isPending={isPending} />
</form>
);
}
// React 19 useFormStatus reads it automatically:
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>Submit</button>;
}
function Form() {
const [state, formAction] = useActionState(submitFormAction, {});
return (
<form action={formAction}>
<input />
<SubmitButton /> {/* No prop needed */}
</form>
);
}Key point: useFormStatus only works inside a <form action={...}> regular <form onSubmit> won't trigger it.
---
useOptimistic()
useOptimistic updates the UI immediately while an async operation is in-flight. When the operation succeeds, the confirmed data replaces the optimistic value. If it fails, the UI reverts.
React 18 Pattern
// React 18 manual optimistic update:
function TodoList({ todos, onAddTodo }) {
const [optimistic, setOptimistic] = useState(todos);
async function handleAddTodo(text) {
const newTodo = { id: Date.now(), text, completed: false };
// Show optimistic update immediately
setOptimistic([...optimistic, newTodo]);
try {
const result = await addTodo(text);
// Update with confirmed result
setOptimistic(prev => [
...prev.filter(t => t.id !== newTodo.id),
result
]);
} catch (err) {
// Revert on error
setOptimistic(optimistic);
}
}
return (
<ul>
{optimistic.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}React 19 useOptimistic() Pattern
import { useOptimistic } from 'react';
async function addTodoAction(prevTodos, formData) {
const text = formData.get('text');
const result = await addTodo(text);
return [...prevTodos, result];
}
function TodoList({ todos }) {
const [optimistic, addOptimistic] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo]
);
const [, formAction] = useActionState(addTodoAction, todos);
async function handleAddTodo(formData) {
const text = formData.get('text');
// Optimistic update:
addOptimistic({ id: Date.now(), text, completed: false });
// Then call the form action:
formAction(formData);
}
return (
<>
<ul>
{optimistic.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
<form action={handleAddTodo}>
<input name="text" />
<button>Add</button>
</form>
</>
);
}Key points:
useOptimistic(currentState, updateFunction)updateFunctionreceives(state, optimisticInput)and returns new state- Call
addOptimistic(input)to trigger the optimistic update - The server action's return value replaces the optimistic state when done
---
Full Example: Todo List with All Hooks
import { useActionState, useFormStatus, useOptimistic } from 'react';
// Server action:
async function addTodoAction(prevTodos, formData) {
const text = formData.get('text');
if (!text) throw new Error('Text required');
const newTodo = await api.post('/todos', { text });
return [...prevTodos, newTodo];
}
// Submit button with useFormStatus:
function AddButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? 'Adding...' : 'Add Todo'}</button>;
}
// Main component:
function TodoApp({ initialTodos }) {
const [optimistic, addOptimistic] = useOptimistic(
initialTodos,
(state, newTodo) => [...state, newTodo]
);
const [todos, formAction] = useActionState(
addTodoAction,
initialTodos
);
async function handleAddTodo(formData) {
const text = formData.get('text');
// Optimistic: show it immediately
addOptimistic({ id: Date.now(), text });
// Then submit the form (which updates when server confirms)
await formAction(formData);
}
return (
<>
<ul>
{optimistic.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
<form action={handleAddTodo}>
<input name="text" placeholder="Add a todo..." required />
<AddButton />
</form>
</>
);
}---
Migration Strategy
Phase 1 No changes required
Actions are opt-in. All existing useReducer + onSubmit patterns continue to work. No forced migration.
Phase 2 Identify refactor candidates
After React 19 migration stabilizes, profile for useReducer + async patterns:
grep -rn "useReducer.*case.*'loading\|useReducer.*case.*'success" src/ --include="*.js" --include="*.jsx"Patterns worth refactoring:
- Form submissions with loading/error state
- Async operations triggered by user events
- Current code uses
dispatch({ type: '...' }) - Simple state shape (object with
loading,error,data)
Phase 3 Refactor to useActionState
// Before:
function LoginForm() {
const [state, dispatch] = useReducer(loginReducer, { loading: false, error: null, user: null });
async function handleSubmit(e) {
e.preventDefault();
dispatch({ type: 'loading' });
try {
const user = await login(e.target);
dispatch({ type: 'success', data: user });
} catch (err) {
dispatch({ type: 'error', error: err.message });
}
}
return <form onSubmit={handleSubmit}>...</form>;
}
// After:
async function loginAction(prevState, formData) {
try {
const user = await login(formData);
return { user, error: null };
} catch (err) {
return { user: null, error: err.message };
}
}
function LoginForm() {
const [state, formAction] = useActionState(loginAction, { user: null, error: null });
return <form action={formAction}>...</form>;
}---
Comparison Table
| Feature | React 18 | React 19 |
|---|---|---|
| Form handling | onSubmit + useReducer | action + useActionState |
| Loading state | Manual dispatch | Automatic isPending |
| Child component pending state | Prop drilling | useFormStatus hook |
| Optimistic updates | Manual state dance | useOptimistic hook |
| Error handling | Manual in dispatch | Return from action |
| Complexity | More boilerplate | Less boilerplate |
React 19 Suspense for Data Fetching Pattern Reference
React 19's new Suspense integration for data fetching is a preview feature that allows components to suspend (pause rendering) until data is available, without useEffect + state.
Important: This is a preview it requires specific setup and is not yet stable for production, but you should know the pattern for React 19 migration planning.
---
What Changed in React 19?
React 18 Suspense only supported code splitting (lazy components). React 19 extends it to data fetching if certain conditions are met:
- Lib usage the data fetching library must implement Suspense (e.g., React Query 5+, SWR, Remix loaders)
- Or your own promise tracking wrap promises in a way React can track their suspension
- No more "no hook after suspense" you can use Suspense directly in components with
use()
---
React 18 Suspense (Code Splitting Only)
// React 18 Suspense for lazy imports only:
const LazyComponent = React.lazy(() => import('./Component'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>
);
}Trying to suspend for data in React 18 required hacks or libraries:
// React 18 hack not recommended:
const dataPromise = fetchData();
const resource = {
read: () => {
throw dataPromise; // Throw to suspend
}
};
function Component() {
const data = resource.read(); // Throws promise → Suspense catches it
return <div>{data}</div>;
}---
React 19 Suspense for Data Fetching (Preview)
React 19 provides first-class support for Suspense with promises via the use() hook:
// React 19 Suspense for data fetching:
function UserProfile({ userId }) {
const user = use(fetchUser(userId)); // Suspends if promise pending
return <div>{user.name}</div>;
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserProfile userId={123} />
</Suspense>
);
}Key differences from React 18:
use()unwraps the promise, component suspends automatically- No need for
useEffect + statetrick - Cleaner code, less boilerplate
---
Pattern 1: Simple Promise Suspense
// Raw promise (not recommended in production):
function DataComponent() {
const data = use(fetch('/api/data').then(r => r.json()));
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<DataComponent />
</Suspense>
);
}Problem: Promise is recreated every render. Solution: wrap in useMemo.
---
Pattern 2: Memoized Promise (Better)
function DataComponent({ id }) {
// Only create promise once per id:
const dataPromise = useMemo(() =>
fetch(`/api/data/${id}`).then(r => r.json()),
[id]
);
const data = use(dataPromise);
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
function App() {
const [id, setId] = useState(1);
return (
<Suspense fallback={<Spinner />}>
<DataComponent id={id} />
<button onClick={() => setId(id + 1)}>Next</button>
</Suspense>
);
}---
Pattern 3: Library Integration (React Query)
Modern data libraries support Suspense directly. React Query 5+ example:
// React Query 5+ with Suspense:
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
// useSuspenseQuery throws promise if suspended
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
return <div>{user.name}</div>;
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserProfile userId={123} />
</Suspense>
);
}Advantage: Library handles caching, retries, and cache invalidation.
---
Pattern 4: Error Boundary Integration
Combine Suspense with Error Boundary to handle both loading and errors:
function UserProfile({ userId }) {
const user = use(fetchUser(userId)); // Suspends while loading
return <div>{user.name}</div>;
}
function App() {
return (
<ErrorBoundary fallback={<ErrorScreen />}>
<Suspense fallback={<Spinner />}>
<UserProfile userId={123} />
</Suspense>
</ErrorBoundary>
);
}
class ErrorBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error };
}
render() {
if (this.state.error) return this.props.fallback;
return this.props.children;
}
}---
Nested Suspense Boundaries
Use multiple Suspense boundaries to show partial UI while waiting for different data:
function App({ userId }) {
return (
<div>
<Suspense fallback={<UserSpinner />}>
<UserProfile userId={userId} />
</Suspense>
<Suspense fallback={<PostsSpinner />}>
<UserPosts userId={userId} />
</Suspense>
</div>
);
}
function UserProfile({ userId }) {
const user = use(fetchUser(userId));
return <h1>{user.name}</h1>;
}
function UserPosts({ userId }) {
const posts = use(fetchUserPosts(userId));
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}Now:
- User profile shows spinner while loading
- Posts show spinner independently
- Both can render as they complete
---
Sequential vs Parallel Suspense
Sequential (wait for first before fetching second)
function App({ userId }) {
const user = use(fetchUser(userId)); // Must complete first
return (
<Suspense fallback={<PostsSpinner />}>
<UserPosts userId={user.id} /> {/* Depends on user */}
</Suspense>
);
}
function UserPosts({ userId }) {
const posts = use(fetchUserPosts(userId));
return <ul>{posts.map(p => <li>{p.title}</li>)}</ul>;
}Parallel (fetch both at once)
function App({ userId }) {
return (
<div>
<Suspense fallback={<UserSpinner />}>
<UserProfile userId={userId} />
</Suspense>
<Suspense fallback={<PostsSpinner />}>
<UserPosts userId={userId} /> {/* Fetches in parallel */}
</Suspense>
</div>
);
}---
Migration Strategy for React 18 → React 19
Phase 1 No changes required
Suspense is still optional and experimental for data fetching. All existing useEffect + state patterns continue to work.
Phase 2 Wait for stability
Before adopting Suspense data fetching in production:
- Wait for React 19 to ship (not preview)
- Verify your data library supports Suspense
- Plan migration after app stabilizes on React 19 core
Phase 3 Refactor to Suspense (optional, post-preview)
Once stable, profile candidates:
grep -rn "useEffect.*fetch\|useEffect.*axios\|useEffect.*graphql" src/ --include="*.js" --include="*.jsx"// Before (React 18):
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
if (!user) return <Spinner />;
return <div>{user.name}</div>;
}
// After (React 19 with Suspense):
function UserProfile({ userId }) {
const user = use(fetchUser(userId));
return <div>{user.name}</div>;
}
// Must be wrapped in Suspense:
<Suspense fallback={<Spinner />}>
<UserProfile userId={123} />
</Suspense>---
Important Warnings
1. Still Preview Suspense for data is marked experimental, behavior may change 2. Performance promises are recreated on every render without memoization; use useMemo 3. Cache use() doesn't cache; use React Query or similar for production apps 4. SSR Suspense SSR support is limited; check Next.js version requirements
React 19 use() Hook Pattern Reference
The use() hook is React 19's answer for unwrapping promises and context within React components. It enables cleaner async patterns directly in your component body, avoiding the architectural complexity that previously required separate lazy components or complex state management.
What is use()?
use() is a hook that:
- Accepts a promise or context object
- Returns the resolved value or context value
- Handles Suspense automatically for promises
- Can be called conditionally inside components (not at top level for promises)
- Throws errors, which Suspense + error boundary can catch
use() with Promises
React 18 Pattern
// React 18 approach 1 lazy load a component module:
const UserComponent = React.lazy(() => import('./User'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserComponent />
</Suspense>
);
}
// React 18 approach 2 fetch data with state + useEffect:
function App({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
if (!user) return <Spinner />;
return <User user={user} />;
}React 19 use() Pattern
// React 19 use() directly in component:
function App({ userId }) {
const user = use(fetchUser(userId)); // Suspends automatically
return <User user={user} />;
}
// Usage:
function Root() {
return (
<Suspense fallback={<Spinner />}>
<App userId={123} />
</Suspense>
);
}Key differences:
use()unwraps the promise directly in the component body- Suspense boundary is still needed, but can be placed at app root (not per-component)
- No state or useEffect needed for simple async data
- Conditional wrapping allowed inside components
use() with Promises -- Conditional Fetching
// React 18 conditional with state
function SearchResults() {
const [results, setResults] = useState(null);
const [query, setQuery] = useState('');
useEffect(() => {
if (query) {
search(query).then(setResults);
} else {
setResults(null);
}
}, [query]);
if (!results) return null;
return <Results items={results} />;
}
// React 19 use() with conditional
function SearchResults() {
const [query, setQuery] = useState('');
if (!query) return null;
const results = use(search(query)); // Only fetches if query is truthy
return <Results items={results} />;
}use() with Context
use() can unwrap context without being at component root. Less common than promise usage, but useful for conditional context reading:
// React 18 always in component body, only works at top level
const theme = useContext(ThemeContext);
// React 19 can be conditional
function Button({ useSystemTheme }) {
const theme = useSystemTheme ? use(ThemeContext) : defaultTheme;
return <button style={theme}>Click</button>;
}---
Migration Strategy
Phase 1 No changes required
React 19 use() is opt-in. All existing Suspense + component splitting patterns continue to work:
// Keep this as-is if it's working:
const Lazy = React.lazy(() => import('./Component'));
<Suspense fallback={<Spinner />}><Lazy /></Suspense>Phase 2 Post-migration cleanup (optional)
After React 19 migration stabilizes, profile codebases for useEffect + state async patterns. These are good candidates for use() refactoring:
Identify patterns:
grep -rn "useEffect.*\(.*fetch\|async\|promise" src/ --include="*.js" --include="*.jsx"Target:
- Simple fetch-on-mount patterns
- No complex dependency arrays
- Single promise per component
- Suspense already in use elsewhere in the app
Example refactor:
// Before:
function Post({ postId }) {
const [post, setPost] = useState(null);
useEffect(() => {
fetchPost(postId).then(setPost);
}, [postId]);
if (!post) return <Spinner />;
return <PostContent post={post} />;
}
// After:
function Post({ postId }) {
const post = use(fetchPost(postId));
return <PostContent post={post} />;
}
// And ensure Suspense at app level:
<Suspense fallback={<AppSpinner />}>
<Post postId={123} />
</Suspense>---
Error Handling
use() throws errors, which Suspense error boundaries catch:
function Root() {
return (
<ErrorBoundary fallback={<ErrorScreen />}>
<Suspense fallback={<Spinner />}>
<DataComponent />
</Suspense>
</ErrorBoundary>
);
}
function DataComponent() {
const data = use(fetchData()); // If fetch rejects, error boundary catches it
return <Data data={data} />;
}---
When NOT to use use()
- Avoid during migration stabilize React 19 first
- Complex dependencies if multiple promises or complex ordering logic, stick with
useEffect - Retry logic
use()doesn't handle retry;useEffectwith state is clearer - Debounced updates
use()refetches on every prop change;useEffectwith cleanup is better
Related skills
How it compares
Use react19-concurrent-patterns for React 19-native form state; prefer older hook patterns only when locked to pre-19 React versions.
FAQ
What does react19-concurrent-patterns cover?
Preserving React 18 concurrent APIs and adopting React 19 use(), Actions, useOptimistic, and Suspense data fetching after migration.
When should I use it?
During or after a React 19 migration when you need to verify concurrent patterns survived and plan new API adoption.
Is react19-concurrent-patterns safe to install?
Review the Security Audits panel on this page before installing in production.