feat: Adds React 18 and 19 migration plugin (#1339)

- Adds React 18 and 19 migration orchestration plugins
- Introduces comprehensive upgrade toolkits for migrating legacy React 16/17 and 18 codebases to React 18.3.1 and 19, respectively. Each plugin bundles specialized agents and skills for exhaustive audit, dependency management, class/component API migration, test suite transformation, and batching regression fixes.
- The React 18 toolkit targets class-component-heavy apps, ensures safe lifecycle and context transitions, resolves dependency blockers, and fully automates test migrations including Enzyme removal. The React 19 toolkit addresses breaking changes such as removal of legacy APIs, defaultProps on function components, and forwardRef, while enforcing a gated, memory-resumable migration pipeline.
- Both plugins update documentation, plugin registries, and skill references to support reliable, repeatable enterprise-scale React migrations.
This commit is contained in:
Saravanan Rajaraman
2026-04-09 10:48:52 +05:30
committed by GitHub
parent f4909cd581
commit 7f7b1b9b46
50 changed files with 8162 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
---
name: react19-concurrent-patterns
description: 'Preserve React 18 concurrent patterns and adopt React 19 APIs (useTransition, useDeferredValue, Suspense, use(), useOptimistic, Actions) during migration.'
---
# 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:
```jsx
// 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:
```jsx
// React 18 useTransition unchanged in React 19:
const [isPending, startTransition] = useTransition();
function handleClick() {
startTransition(() => {
setFilteredResults(computeExpensiveFilter(input));
});
}
```
### useDeferredValue No Migration Needed
```jsx
// React 18 useDeferredValue unchanged in React 19:
const deferredQuery = useDeferredValue(query);
```
### Suspense for Code Splitting No Migration Needed
```jsx
// 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**:
```bash
# 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.

View File

@@ -0,0 +1,371 @@
---
title: React 19 Actions Pattern Reference
---
# 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
```jsx
// 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
```jsx
// 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
- `formAction` replaces `onSubmit`, form automatically collects FormData
- `isPending` is 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.
```jsx
// 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
```jsx
// 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
```jsx
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)`
- `updateFunction` receives `(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
```jsx
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:
```bash
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
```jsx
// 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 |

View File

@@ -0,0 +1,335 @@
---
title: React 19 Suspense for Data Fetching Pattern Reference
---
# 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)
```jsx
// 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:
```jsx
// 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:
```jsx
// 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 + state` trick
- Cleaner code, less boilerplate
---
## Pattern 1: Simple Promise Suspense
```jsx
// 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)
```jsx
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:
```jsx
// 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:
```jsx
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:
```jsx
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)
```jsx
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)
```jsx
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:
```bash
grep -rn "useEffect.*fetch\|useEffect.*axios\|useEffect.*graphql" src/ --include="*.js" --include="*.jsx"
```
```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

View File

@@ -0,0 +1,208 @@
---
title: React 19 use() Hook Pattern Reference
---
# 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
```jsx
// 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
```jsx
// 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
```jsx
// 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:
```jsx
// 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:
```jsx
// 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:
```bash
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:
```jsx
// 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:
```jsx
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; `useEffect` with state is clearer
- **Debounced updates** `use()` refetches on every prop change; `useEffect` with cleanup is better