React 19 Interview Questions: New Features You Must Know

·20 min read
ReactReact 19Interview QuestionsuseActionStateuseOptimisticReact HooksFrontendJavaScript2025

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.

The 30-Second Overview

If an interviewer asks "What's new in React 19?" and you only have 30 seconds, here's what separates informed candidates from those still running on outdated knowledge:

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? React 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.

The 2-Minute Deep Dive

When you have more time, expand on how these features work together. React 19's philosophy centers on making common patterns declarative rather than imperative. Let me walk you through the mental model shift.

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.

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.

useEffectEvent addresses a pain point that's plagued hooks since their introduction. When effects need to call callbacks that read current state, you either add the callback to dependencies (causing re-runs) or disable the lint rule (risking stale values). Effect Events give you a principled escape hatch: functions that always see current values but don't trigger effect re-execution.

Question 1: What is useActionState and How Does It Work?

This question has become a litmus test for whether candidates have actually built anything with React 19 or just read the release notes.

Weak answer: "It's a hook for managing form state."

Strong answer: 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.

Let me show you the transformation with code:

// 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 interviewers are looking for: Understanding that useActionState encapsulates the pending-success-error cycle. Candidates who explain the data flow (action returns state, isPending reflects execution) demonstrate they've internalized the model.

Common follow-up: "When would you still use the old 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.

Question 2: How Does useOptimistic Enable Better UX?

This question tests whether candidates understand the UX implications of async operations and how React 19 addresses perceived latency.

Weak answer: "It updates the UI before the server responds."

Strong answer: 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.

Here's where it gets interesting:

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>
    );
}

The second argument to useOptimistic is a reducer function that describes how to apply optimistic updates. This is powerful because it handles complex transformations, not just simple value swaps.

A pattern that's served me well: combine 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
    ]
);

What interviewers are looking for: Understanding that useOptimistic is about perceived performance, not actual performance. The operation takes the same time, but users feel the interface is faster. Bonus points for discussing rollback behavior on errors.

Common follow-up: "What happens if the server request fails?" Strong candidates explain that React reverts to the server state automatically—the optimistic value was never "real," just a preview of what we expected.

Question 3: Explain Form Actions and How They Change Form Handling

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

Weak answer: "You can pass a function to the form's action prop instead of using onSubmit."

Strong answer: 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.

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.

Here's the mental model: 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.

What interviewers are looking for: Recognition that Actions are more than syntax sugar—they enable new composition patterns like useFormStatus. Candidates who discuss the declarative vs. imperative shift demonstrate deeper understanding.

Common follow-up: "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.

Question 4: Why Did React 19 Remove the Need for forwardRef?

This question reveals whether candidates understand why forwardRef existed in the first place and what changed architecturally.

Weak answer: "They just let you pass ref as a prop now."

Strong answer: 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.

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.

What interviewers are looking for: Understanding the "why" behind the change—that refs being special was a limitation, not a feature. Candidates who mention the TypeScript and debugging improvements show practical experience with the pain points.

Common follow-up: "How does this 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.

Question 5: What Problem Does useEffectEvent Solve?

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

Weak answer: "It creates functions that don't need to be in the dependency array."

Strong answer: 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.

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.

What interviewers are looking for: Understanding the specific problem (effects re-running when they shouldn't) and why the solutions before useEffectEvent were unsatisfying. Candidates who can articulate "read current value without causing re-run" demonstrate true comprehension.

Common follow-up: "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.

Question 6: What Are the New Resource Preloading APIs?

This question tests awareness of performance-focused features that don't get as much attention as hooks.

Weak answer: "React added some preloading functions."

Strong answer: 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' });
 
    // ...
}

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.

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} />;
}

What interviewers are looking for: Understanding the differences between the four APIs and when to use each. Candidates who mention automatic deduplication and head hoisting show they've read the implementation details.

Common follow-up: "How is this different from adding link tags manually?" 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.

Question 7: What Is the Activity Component in React 19.2?

This is a forward-looking question for candidates tracking React's development.

Weak answer: "I'm not sure what that is."

Strong answer: 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>
    );
}

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.

What interviewers are looking for: 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.

Common follow-up: "How does this 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.

What Interviewers Actually Care About

After covering these specific features, I want to share the meta-pattern I've observed. When interviewers ask about React 19, they're testing several things simultaneously.

First, currency—are you actively learning, or did you stop paying attention after you got comfortable with hooks? React evolves, and developers who don't evolve with it write increasingly outdated code.

Second, depth of understanding. Anyone can memorize that useActionState returns [state, action, isPending]. Fewer candidates can explain why that signature exists, what problems it solves, and when you might still prefer the manual approach.

Third, practical application. The candidates who stand out don't just recite features—they share experiences. "We migrated our checkout form to useActionState and eliminated 40 lines of boilerplate" is more compelling than a textbook definition.

Code Examples to Study

Let me give you a complete component that uses multiple React 19 features together. This is the kind of integrated example that demonstrates real-world usage:

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. It's the kind of code that shows you understand how React 19 features compose.

Quick Reference Table

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

Practice Questions

Test your understanding with these questions. Try answering them before looking at the answers below.

Question 1: A form submission currently takes 2 seconds. Using only React 19 features, how would you make the UI feel instant while maintaining data consistency?

Answer: 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.

Question 2: Your useEffect needs to log analytics with the current user's plan level, but you don't want to reconnect to a WebSocket when the plan level changes. How do you solve this in React 19?

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

Question 3: You're building a component library that exposes DOM refs. How should you handle this in React 19, and what migration path should you communicate to users?

Answer: Accept ref as a regular prop instead of using forwardRef. For migration, release this as a major version since it changes the component API. Document that users should pass ref like any other prop: <Input ref={myRef} /> rather than the previous syntax.

Question 4: A page loads slowly because it waits for a third-party API. The API domain is known ahead of time. What React 19 API would help, and where would you call it?

Answer: Use preconnect('https://third-party-api.com') at the top of your component or in a parent component. This initiates DNS lookup, TCP connection, and TLS handshake before you actually need to fetch data, reducing perceived load time.


Related Articles

If you found this helpful, check out these related guides:

Next Steps

Understanding React 19 features is table stakes for modern React interviews. The candidates who stand out are those who've actually used these patterns in real applications and can discuss the tradeoffs.

If you want to deepen your React interview preparation, our comprehensive question set covers everything from basic concepts to advanced patterns like the ones discussed here. We update it regularly as React evolves.

The shift to React 19 patterns is happening now. Teams are updating their codebases, and interviewers are updating their questions. Make sure your knowledge has been updated too.

Ready to ace your interview?

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

View PDF Guides