35+ React 19 Interview Questions 2025: useActionState, useOptimistic & Form Actions

·19 min read
reactreact-19frontenduseActionStateuseOptimisticjavascriptinterview-preparation

React 19 eliminated patterns that developers have copy-pasted for years. The useState + try-catch + isLoading pattern for forms? Replaced by useActionState. The forwardRef wrapper? Gone—refs are just props now. The stale closure problem in useEffect? Solved by useEffectEvent.

If you're still writing 2023-style React in 2025, interviewers will notice. This guide covers the React 19 features that actually appear in technical interviews, showing the old patterns versus the new ones and explaining why React made these fundamental changes.

Table of Contents

  1. React 19 Overview Questions
  2. useActionState Questions
  3. useOptimistic Questions
  4. Form Actions Questions
  5. Ref as Prop Questions
  6. useEffectEvent Questions
  7. Resource Preloading Questions
  8. Activity Component Questions
  9. Integrated Patterns Questions

React 19 Overview Questions

Understanding React 19's philosophy helps you answer any specific question about its features.

What are the major changes in React 19?

React 19 introduced Actions—a new paradigm for handling async operations, especially forms. The useActionState hook manages pending states and errors automatically. The useOptimistic hook enables instant UI feedback. Forms can now pass functions directly to the action prop instead of onSubmit. Refs work as regular props without forwardRef. And useEffectEvent finally solves the stale closure problem in effects.

The theme across all these changes is eliminating boilerplate that developers have been writing for years. If your interview answer involves "you'd useState for loading, useState for error, try-catch in the handler...", you're describing the pattern these features replace.

What is React 19's philosophy on form handling?

Before React 19, form handling required orchestrating multiple pieces of state. You needed a loading state, an error state, form data state, and an event handler that coordinated them all. This pattern appeared in virtually every form, yet developers wrote it from scratch each time.

React 19 recognizes that data mutations follow predictable patterns: start pending, execute async operation, handle success or error, update UI. The new hooks encode these patterns directly, letting you describe what should happen rather than manually implementing the state machine.

How does React 19 treat refs differently?

The ref change removes an API that always felt like a workaround. forwardRef existed because refs were "special"—they couldn't flow through components like regular props. Now they can. This isn't just syntactic sugar; it reflects React treating refs as first-class values.

React 19 changes the internal implementation so refs can flow through components like any other prop. This has implications for simpler component APIs, better TypeScript inference, and cleaner debugging.


useActionState Questions

This hook has become a litmus test for whether candidates have actually built anything with React 19.

What is useActionState and how does it work?

useActionState is React 19's solution to form handling complexity. It takes an async action function and initial state, then returns three values: the current state, a submit action you pass to forms, and an isPending boolean for loading indicators.

The key insight is that useActionState handles the entire submission lifecycle. You don't manually set loading to true, await the operation, set loading to false, and catch errors separately. The hook does this automatically, and the isPending value stays true exactly as long as the action is executing.

Here's the transformation from the old pattern to React 19:

// The old pattern - verbose and error-prone
function UpdateProfile() {
    const [name, setName] = useState('');
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(null);
 
    async function handleSubmit(e) {
        e.preventDefault();
        setIsLoading(true);
        setError(null);
        try {
            await updateProfile({ name });
        } catch (err) {
            setError(err.message);
        } finally {
            setIsLoading(false);
        }
    }
 
    return (
        <form onSubmit={handleSubmit}>
            <input
                value={name}
                onChange={e => setName(e.target.value)}
            />
            <button disabled={isLoading}>
                {isLoading ? 'Saving...' : 'Save'}
            </button>
            {error && <p className="error">{error}</p>}
        </form>
    );
}

Now here's the same functionality with useActionState:

// React 19 - declarative and concise
function UpdateProfile() {
    const [error, submitAction, isPending] = useActionState(
        async (previousState, formData) => {
            const result = await updateProfile({
                name: formData.get('name')
            });
            if (result.error) {
                return result.error;
            }
            redirect('/profile');
            return null;
        },
        null
    );
 
    return (
        <form action={submitAction}>
            <input type="text" name="name" />
            <button disabled={isPending}>
                {isPending ? 'Saving...' : 'Save'}
            </button>
            {error && <p className="error">{error}</p>}
        </form>
    );
}

Notice several important changes. First, we pass a function to action instead of onSubmit—this is form Actions at work. Second, the action receives formData automatically, so we don't need controlled inputs for simple forms. Third, whatever the action returns becomes the new state, giving us error handling without try-catch.

What does useActionState return?

The hook returns a tuple of three values: [state, submitAction, isPending]. The state contains whatever your action function returns—typically error messages or null for success. The submitAction is a function you pass to the form's action prop. The isPending boolean indicates whether the action is currently executing.

This signature encapsulates the pending-success-error cycle that every form needs. Candidates who can explain this data flow (action returns state, isPending reflects execution) demonstrate they've internalized the model.

When would you still use the old form handling pattern?

Good answers mention complex multi-step forms, real-time validation, or when you need fine-grained control over the submission timing. useActionState optimizes for common cases, not every case.

For example, if you need to validate fields as the user types and show errors before submission, you still need controlled inputs with useState. If you need to submit in multiple stages or coordinate multiple API calls, manual state management may be clearer.


useOptimistic Questions

These questions test whether candidates understand the UX implications of async operations.

How does useOptimistic enable better UX?

useOptimistic solves a fundamental UX problem: users perceive interfaces as slow when they have to wait for server confirmation before seeing their actions reflected. The solution is optimistic updates—show the expected result immediately, then reconcile when the server responds.

Before React 19, implementing this required careful state management. You'd track both the "real" state and the "optimistic" state, merge them for display, and handle rollback on errors. It was doable but tedious and error-prone.

useOptimistic makes this pattern declarative. You provide the current server state and get back an optimistic state value plus an updater function. When you call the updater, the optimistic state changes immediately. When the async operation completes (success or failure), React automatically reconciles the states.

function TodoList({ todos, onToggle }) {
    const [optimisticTodos, toggleOptimistic] = useOptimistic(
        todos,
        (currentTodos, todoId) =>
            currentTodos.map(todo =>
                todo.id === todoId
                    ? { ...todo, completed: !todo.completed }
                    : todo
            )
    );
 
    async function handleToggle(todoId) {
        toggleOptimistic(todoId);  // UI updates instantly
        await onToggle(todoId);    // Server call happens in background
    }
 
    return (
        <ul>
            {optimisticTodos.map(todo => (
                <li
                    key={todo.id}
                    onClick={() => handleToggle(todo.id)}
                    style={{
                        textDecoration: todo.completed ? 'line-through' : 'none',
                        opacity: todo.sending ? 0.5 : 1  // Visual pending state
                    }}
                >
                    {todo.text}
                </li>
            ))}
        </ul>
    );
}

What are the arguments to useOptimistic?

The hook takes two arguments: the current "real" state (typically from a server or parent component) and a reducer function that describes how to apply optimistic updates. The reducer receives the current optimistic state and the value passed to the updater, returning the new optimistic state.

This is powerful because it handles complex transformations, not just simple value swaps. You can add items to lists, toggle booleans, update nested objects—any transformation expressible as a pure function.

How can you show a pending indicator with optimistic updates?

A pattern that works well is combining useOptimistic with a "sending" flag to give users subtle feedback that their action is in progress while still showing the expected outcome:

const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
        ...state,
        { text: newMessage, sending: true }  // Flag for pending indicator
    ]
);

The optimistic item appears immediately with a visual indicator (like reduced opacity or a spinner). When the server confirms, the real state updates and the optimistic version is replaced.

What happens if the server request fails with useOptimistic?

React reverts to the server state automatically—the optimistic value was never "real," just a preview of what we expected. This is the key insight: optimistic updates are temporary projections that get replaced by actual data.

The hook is about perceived performance, not actual performance. The operation takes the same time, but users feel the interface is faster because they see their action reflected immediately.


Form Actions Questions

Form Actions represent a paradigm shift that many developers miss because they look like a small syntax change.

What are form Actions in React 19?

Form Actions are React 19's way of making form handling declarative. Instead of imperatively preventing default behavior, managing FormData, and coordinating state updates, you describe the action to take and let React handle the plumbing.

The action prop accepts a function that receives FormData automatically. React handles calling preventDefault(), extracting form values, and integrating with hooks like useActionState and useFormStatus. This isn't just convenience—it enables patterns that weren't possible before.

How does useFormStatus work with form Actions?

Consider how Actions compose with useFormStatus:

// useFormStatus only works inside form Actions
function SubmitButton() {
    const { pending } = useFormStatus();
 
    return (
        <button type="submit" disabled={pending}>
            {pending ? 'Submitting...' : 'Submit'}
        </button>
    );
}
 
function ContactForm() {
    async function submitForm(formData) {
        await sendMessage({
            email: formData.get('email'),
            message: formData.get('message')
        });
    }
 
    return (
        <form action={submitForm}>
            <input type="email" name="email" required />
            <textarea name="message" required />
            <SubmitButton />  {/* Knows if parent form is pending */}
        </form>
    );
}

The useFormStatus hook reads pending state from the parent form automatically. You couldn't do this cleanly before—you'd have to pass isLoading as a prop or use context. Now the button "just knows" because form Actions create an implicit context.

What is the mental model shift with form Actions?

Actions turn forms from "containers of inputs with an event handler" into "descriptions of data mutations." This aligns with React's broader philosophy of declarative UI. You declare what data transformation should happen, not how to orchestrate the DOM events.

Recognition that Actions are more than syntax sugar—they enable new composition patterns like useFormStatus—demonstrates deeper understanding of React 19's design.

Can you use Actions with controlled inputs?

Yes, absolutely. Actions don't require uncontrolled inputs; you can still use useState for real-time validation or complex input logic. Actions just give you another option that's often simpler for basic forms.

If you need to validate on every keystroke, transform input values, or coordinate multiple fields, controlled inputs remain the right choice. Actions shine for straightforward submission flows.


Ref as Prop Questions

These questions reveal whether candidates understand why forwardRef existed in the first place.

Why did React 19 remove the need for forwardRef?

To understand why this matters, you need to know why forwardRef existed. In earlier React, refs were "special"—they weren't part of the regular props object because React needed to handle them differently during reconciliation. forwardRef was an API workaround that let you "forward" this special value through component boundaries.

React 19 changes the internal implementation so refs can flow through components like any other prop. This has several implications.

First, simpler component APIs:

// Before React 19 - forwardRef wrapper required
const Input = forwardRef(function Input({ label, ...props }, ref) {
    return (
        <label>
            {label}
            <input ref={ref} {...props} />
        </label>
    );
});
 
// React 19 - ref is just another prop
function Input({ label, ref, ...props }) {
    return (
        <label>
            {label}
            <input ref={ref} {...props} />
        </label>
    );
}

Second, better TypeScript inference. forwardRef had notorious typing issues because TypeScript struggled with its higher-order function signature. With ref as a prop, types flow naturally.

Third, cleaner debugging. Component names in DevTools and error messages were sometimes awkward with forwardRef. Regular function components don't have this issue.

Is forwardRef deprecated in React 19?

The React team announced that forwardRef will be deprecated in a future version. While it still works in React 19, new code should use the prop pattern. If you're maintaining a component library, this is a significant API surface change to plan for.

How does the ref as prop change affect existing codebases?

Existing forwardRef usage continues to work. Migration is optional but recommended for new components. Libraries will need major version bumps to change their APIs because consumers will need to update how they pass refs.

Understanding the "why" behind the change—that refs being special was a limitation, not a feature—demonstrates practical experience with the pain points forwardRef caused.


useEffectEvent Questions

This is a nuanced topic that separates developers who've hit the stale closure problem from those who've only read about hooks.

What problem does useEffectEvent solve?

useEffectEvent solves a fundamental tension in the hooks model. Effects should re-run when their dependencies change—that's the rule. But sometimes effects need to call functions that read current props or state without wanting the effect to re-run when those values change.

A concrete example makes this clear:

// The problem: effect re-runs whenever theme changes
function ChatRoom({ roomId, theme }) {
    useEffect(() => {
        const connection = createConnection(roomId);
        connection.on('connected', () => {
            showNotification('Connected!', theme);  // Uses theme
        });
        connection.connect();
        return () => connection.disconnect();
    }, [roomId, theme]);  // theme in deps causes reconnect on theme change!
}

This effect should only re-run when roomId changes (reconnect to new room). But the notification handler uses theme, so the linter correctly warns you to add it as a dependency. The result? Changing themes disconnects and reconnects—clearly wrong behavior.

How does useEffectEvent fix the stale closure problem?

Before useEffectEvent, developers either disabled the lint rule (risky) or created convoluted workarounds with refs. useEffectEvent gives you a principled solution:

// The solution: Effect Events read current values without being dependencies
function ChatRoom({ roomId, theme }) {
    const onConnected = useEffectEvent(() => {
        showNotification('Connected!', theme);  // Always reads current theme
    });
 
    useEffect(() => {
        const connection = createConnection(roomId);
        connection.on('connected', () => {
            onConnected();  // Not a dependency
        });
        connection.connect();
        return () => connection.disconnect();
    }, [roomId]);  // Only roomId - correct!
}

The function returned by useEffectEvent is stable (same reference across renders) but always "sees" the latest values when called. It's not in the dependency array because it's not reactive—it doesn't determine when the effect runs, only what happens when it does.

Is useEffectEvent the same as useCallback with an empty dependency array?

No—that's the trap answer. useCallback(fn, []) gives you a stable function that closes over stale values. useEffectEvent gives you a stable function that always reads current values. The difference is crucial.

With useCallback(fn, []), the function captures values from the first render and never updates. With useEffectEvent, the function always reads the current values at call time, even though its identity stays stable.


Resource Preloading Questions

These questions test awareness of performance-focused features that don't get as much attention as hooks.

What resource preloading APIs were added in React 19?

React 19 added four resource preloading APIs to react-dom: prefetchDNS, preconnect, preload, and preinit. These let you optimize page loads by telling the browser about resources it will need.

The distinction between them matters:

import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';
 
function App() {
    // DNS lookup only - when you might request from this host
    prefetchDNS('https://api.example.com');
 
    // DNS + TCP + TLS handshake - when you will request but don't know what
    preconnect('https://api.example.com');
 
    // Actually fetch the resource - for fonts, stylesheets, images
    preload('https://fonts.example.com/font.woff2', { as: 'font' });
 
    // Fetch AND execute - for critical scripts
    preinit('https://example.com/critical.js', { as: 'script' });
 
    // ...
}

When would you use each preloading API?

  • prefetchDNS: When you might make requests to a host. Minimal cost, resolves domain name early.
  • preconnect: When you will definitely request from a host but don't know exactly what. Sets up the full connection.
  • preload: When you know a specific resource will be needed. Fetches fonts, stylesheets, images.
  • preinit: When you need a script to run as soon as possible. Fetches and executes.

The practical use case is optimizing initial page loads, especially with code splitting:

function ProductPage({ productId }) {
    // Start loading the product API connection while React renders
    preconnect('https://api.store.com');
 
    // Preload product images we know we'll need
    preload(`https://cdn.store.com/products/${productId}/main.jpg`, {
        as: 'image'
    });
 
    // Preinit analytics that should run ASAP
    preinit('https://analytics.store.com/tracker.js', { as: 'script' });
 
    return <ProductDetails id={productId} />;
}

React automatically hoists these to the document <head> and deduplicates them. If multiple components call preload for the same URL, only one request is made. The order they appear in your code doesn't matter—React prioritizes them by utility to early loading.

Manual tags don't deduplicate, don't prioritize intelligently, and don't integrate with React's rendering. These APIs understand the component lifecycle and can respond to what React is actually rendering.


Activity Component Questions

These are forward-looking questions for candidates tracking React's development.

What is the Activity component in React 19.2?

The Activity component was introduced in React 19.2 (October 2025) as part of React's ongoing work on offscreen rendering. It allows components to be rendered "off-screen" in a hidden state, preserving their state while removing them from the visible UI.

The primary use case is keeping component state alive during navigation. Think of a tab interface where switching tabs unmounts and remounts components, losing their internal state. With Activity, you can hide inactive tabs without destroying them:

function TabbedInterface({ activeTab }) {
    return (
        <div>
            <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>
                <HomeTab />
            </Activity>
            <Activity mode={activeTab === 'search' ? 'visible' : 'hidden'}>
                <SearchTab />  {/* Preserves search results when hidden */}
            </Activity>
            <Activity mode={activeTab === 'profile' ? 'visible' : 'hidden'}>
                <ProfileTab />
            </Activity>
        </div>
    );
}

What happens to effects when Activity mode is hidden?

When mode is 'hidden', the component's effects are paused and it's removed from the accessibility tree, but state and DOM are preserved. When it becomes 'visible' again, effects resume where they left off.

This is particularly powerful for mobile-style navigation where you want the "back" behavior to restore the previous screen exactly as it was, including scroll position and form inputs.

How does Activity relate to Suspense and Transitions?

They're all part of React's concurrent features. Activity extends the model by letting you explicitly manage when components are active vs. preserved-but-hidden. While Suspense handles loading states and Transitions handle priority, Activity handles visibility.

Awareness of React's cutting-edge features demonstrates you're following the ecosystem actively. Even if you haven't used Activity in production, knowing it exists and understanding the use case shows engagement.


Integrated Patterns Questions

These questions test how well you can combine React 19 features.

How would you build a comment section using multiple React 19 features?

Here's a complete component that uses multiple React 19 features together:

import { useActionState, useOptimistic } from 'react';
import { prefetchDNS, preconnect } from 'react-dom';
 
function CommentSection({ postId, initialComments }) {
    // Preload connection to comment API
    preconnect('https://api.example.com');
 
    // Optimistic comments for instant feedback
    const [optimisticComments, addOptimisticComment] = useOptimistic(
        initialComments,
        (comments, newComment) => [
            ...comments,
            { ...newComment, sending: true }
        ]
    );
 
    // Form action with integrated state management
    const [error, submitComment, isPending] = useActionState(
        async (prevState, formData) => {
            const text = formData.get('comment');
 
            // Show optimistic comment immediately
            addOptimisticComment({
                id: crypto.randomUUID(),
                text,
                author: 'You'
            });
 
            // Server call
            const result = await postComment(postId, text);
 
            if (result.error) {
                return result.error;  // Optimistic update will rollback
            }
 
            return null;  // Success - no error
        },
        null
    );
 
    return (
        <div>
            <ul>
                {optimisticComments.map(comment => (
                    <li key={comment.id} style={{
                        opacity: comment.sending ? 0.6 : 1
                    }}>
                        <strong>{comment.author}:</strong> {comment.text}
                        {comment.sending && <span> (posting...)</span>}
                    </li>
                ))}
            </ul>
 
            <form action={submitComment}>
                <textarea
                    name="comment"
                    placeholder="Write a comment..."
                    required
                />
                <SubmitButton />
            </form>
 
            {error && <p className="error">{error}</p>}
        </div>
    );
}
 
function SubmitButton() {
    const { pending } = useFormStatus();
 
    return (
        <button type="submit" disabled={pending}>
            {pending ? 'Posting...' : 'Post Comment'}
        </button>
    );
}

This component demonstrates useOptimistic for instant feedback, useActionState for form handling, useFormStatus for button state, form Actions for declarative submission, and resource preloading for performance.

How would you make a 2-second form submission feel instant?

Use useOptimistic to immediately show the expected result, combined with useActionState to handle the actual submission. The optimistic state updates instantly, and if the server call fails, React automatically rolls back to the previous state.

The key is that perceived performance matters more than actual performance. Users see their action reflected immediately, even though the server still takes 2 seconds.

How would you log analytics with current user data without re-running WebSocket effects?

Wrap the analytics logging in useEffectEvent. The Effect Event will always read the current user data (like plan level) when called, but it won't be a dependency of the effect, so changing user data won't cause WebSocket reconnection.


Quick Reference

FeatureHook/APIPrimary Use CaseReplaces
Action StateuseActionStateForm submission handlinguseState + try/catch
Optimistic UpdatesuseOptimisticInstant UI feedbackManual state duplication
Form Actions<form action={fn}>Declarative form handlingonSubmit + preventDefault
Form StatususeFormStatusPending state in childrenProp drilling loading state
Effect EventsuseEffectEventNon-reactive effect callbacksuseRef workarounds
Ref as Propfunction Comp({ ref })DOM referencesforwardRef
DNS PrefetchprefetchDNS()Early DNS resolutionManual link tags
Preconnectpreconnect()Early connection setupManual link tags
Preloadpreload()Early resource fetchManual link tags
Preinitpreinit()Fetch and execute scriptManual script tags
Activity<Activity>Preserve hidden stateConditional rendering

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides