On our cosmic journey through the galaxies of React, forms are one of the most important communication modules of the ship. Every mission report, every command for the navigation system, and every message to base - everything goes through forms. React 19 introduces two revolutionary hooks:
useFormStatus and useActionState, which completely change the way we manage forms.Before we explore the new hooks, we need to understand the concept of Server Actions. These are functions marked with the
'use server' directive that execute on the server side but can be called directly from forms in a client component:1// actions.js
2'use server';
3
4async function submitMissionReport(formData) {
5 const missionName = formData.get('missionName');
6 const status = formData.get('status');
7
8 // This executes on the SERVER
9 await database.missions.create({
10 name: missionName,
11 status: status,
12 timestamp: new Date()
13 });
14
15 return { success: true, message: 'Report saved!' };
16}Server Actions can be passed directly to the
action attribute of a form:1// MissionForm.jsx
2function MissionForm() {
3 return (
4 <form action={submitMissionReport}>
5 <input name="missionName" placeholder="Mission name" />
6 <select name="status">
7 <option value="active">Active</option>
8 <option value="completed">Completed</option>
9 </select>
10 <button type="submit">Submit report</button>
11 </form>
12 );
13}This approach eliminates the need to manually create API endpoints and manage
loading/error states - React handles this automatically.useFormStatus is a hook that returns information about the form submission state. Key rule: it must be used in a child component of a <form> element, not in the form itself.1import { useFormStatus } from 'react-dom';
2
3// Submit button component - INSIDE the form
4function SubmitButton() {
5 const { pending, data, method, action } = useFormStatus();
6
7 return (
8 <button
9 type="submit"
10 disabled={pending}
11 style={{
12 padding: '10px 20px',
13 background: pending ? '#333' : '#00d4ff',
14 color: pending ? '#666' : '#0a0e17',
15 border: 'none',
16 borderRadius: '6px',
17 cursor: pending ? 'wait' : 'pointer'
18 }}
19 >
20 {pending ? 'Sending report...' : 'Submit mission report'}
21 </button>
22 );
23}
24
25// pending - true when the form is being submitted
26// data - FormData object with form data
27// method - HTTP method (GET/POST)
28// action - reference to the action function1import { useFormStatus } from 'react-dom';
2
3function MissionInput({ name, label, placeholder }) {
4 const { pending } = useFormStatus();
5 return (
6 <div style={{ marginBottom: '12px' }}>
7 <label style={{ color: '#8899aa', display: 'block', marginBottom: '4px' }}>
8 {label}
9 </label>
10 <input
11 name={name}
12 placeholder={placeholder}
13 disabled={pending}
14 style={{
15 width: '100%',
16 padding: '8px 12px',
17 background: pending ? '#1a1a2e' : '#0f1729',
18 color: '#e0e1dd',
19 border: '1px solid #2a3a5c',
20 borderRadius: '6px',
21 opacity: pending ? 0.5 : 1
22 }}
23 />
24 </div>
25 );
26}
27
28function LoadingOverlay() {
29 const { pending } = useFormStatus();
30 if (!pending) return null;
31 return (
32 <div style={{
33 position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
34 background: 'rgba(10, 14, 23, 0.7)',
35 display: 'flex', alignItems: 'center', justifyContent: 'center',
36 borderRadius: '8px'
37 }}>
38 <p style={{ color: '#00d4ff' }}>Transmission in progress...</p>
39 </div>
40 );
41}
42
43function MissionReportForm({ submitAction }) {
44 return (
45 <form action={submitAction} style={{ position: 'relative' }}>
46 <LoadingOverlay />
47 <MissionInput name="mission" label="Mission name" placeholder="E.g. Alpha Centauri" />
48 <MissionInput name="coordinates" label="Coordinates" placeholder="E.g. 14h 29m 43s" />
49 <SubmitButton />
50 </form>
51 );
52}useActionState (formerly useFormState) is a hook that combines Server Actions with state management. It returns the current state, a wrapped action, and an isPending flag:1import { useActionState } from 'react';
2
3// Server Action returning new state
4async function createMission(previousState, formData) {
5 const name = formData.get('name');
6 const destination = formData.get('destination');
7
8 if (!name || name.length < 3) {
9 return {
10 error: 'Mission name must be at least 3 characters',
11 success: false
12 };
13 }
14
15 // Simulation of saving to database
16 await new Promise(resolve => setTimeout(resolve, 1000));
17
18 return {
19 success: true,
20 message: 'Mission created successfully!',
21 missionId: Math.random().toString(36).substr(2, 9)
22 };
23}
24
25function CreateMissionForm() {
26 const [state, formAction, isPending] = useActionState(
27 createMission,
28 { error: null, success: false } // Initial state
29 );
30
31 return (
32 <form action={formAction}>
33 <h2 style={{ color: '#00d4ff' }}>New space mission</h2>
34
35 <input name="name" placeholder="Mission name" />
36 <input name="destination" placeholder="Destination" />
37
38 <button type="submit" disabled={isPending}>
39 {isPending ? 'Creating mission...' : 'Create mission'}
40 </button>
41
42 {state.error && (
43 <p style={{ color: '#ff6b6b' }}>{state.error}</p>
44 )}
45
46 {state.success && (
47 <p style={{ color: '#00ff88' }}>{state.message} (ID: {state.missionId})</p>
48 )}
49 </form>
50 );
51}The
useActionState hook accepts three arguments:(previousState, formData) => newStateIt returns an array with three elements:
<form action>One of the greatest advantages of the new form hooks is progressive enhancement - forms work even without client-side JavaScript:
1// This form works BEFORE JavaScript loads!
2function SearchForm() {
3 const [state, searchAction, isPending] = useActionState(
4 async (prev, formData) => {
5 const query = formData.get('query');
6 const results = await searchGalaxy(query);
7 return { results, query };
8 },
9 { results: [], query: '' }
10 );
11
12 return (
13 <form action={searchAction}>
14 <input
15 name="query"
16 defaultValue={state.query}
17 placeholder="Search stars, planets, nebulae..."
18 />
19 <button type="submit" disabled={isPending}>
20 {isPending ? 'Scanning...' : 'Scan galaxy'}
21 </button>
22
23 {state.results.length > 0 && (
24 <ul>
25 {state.results.map(r => (
26 <li key={r.id}>{r.name} - {r.type}</li>
27 ))}
28 </ul>
29 )}
30 </form>
31 );
32}A form submitted before JavaScript loads will be handled by the server, and after hydration React takes over control - the user will not notice any difference.
useActionState is perfect for server-side validation with immediate feedback:1async function validateAndSubmit(prevState, formData) {
2 const errors = {};
3
4 const name = formData.get('crewName');
5 const role = formData.get('role');
6 const experience = parseInt(formData.get('experience'));
7
8 if (!name || name.length < 2) errors.crewName = 'Name must be at least 2 characters';
9 if (!role) errors.role = 'Select a role';
10 if (isNaN(experience) || experience < 0) errors.experience = 'Invalid experience';
11
12 if (Object.keys(errors).length > 0) {
13 return { errors, success: false, values: { name, role, experience } };
14 }
15
16 await saveCrewMember({ name, role, experience });
17 return { errors: {}, success: true, message: 'Crew member added!' };
18}
19
20function CrewRegistrationForm() {
21 const [state, formAction, isPending] = useActionState(
22 validateAndSubmit,
23 { errors: {}, success: false }
24 );
25
26 return (
27 <form action={formAction}>
28 <input name="crewName" defaultValue={state.values?.name || ''} />
29 {state.errors?.crewName && <span style={{color:'red'}}>{state.errors.crewName}</span>}
30
31 <select name="role" defaultValue={state.values?.role || ''}>
32 <option value="">Select a role</option>
33 <option value="pilot">Pilot</option>
34 <option value="engineer">Engineer</option>
35 <option value="scientist">Scientist</option>
36 </select>
37 {state.errors?.role && <span style={{color:'red'}}>{state.errors.role}</span>}
38
39 <input name="experience" type="number" defaultValue={state.values?.experience || ''} />
40 {state.errors?.experience && <span style={{color:'red'}}>{state.errors.experience}</span>}
41
42 <button type="submit" disabled={isPending}>
43 {isPending ? 'Saving...' : 'Add to crew'}
44 </button>
45
46 {state.success && <p style={{color:'green'}}>{state.message}</p>}
47 </form>
48 );
49}The new form hooks in React 19 are a fundamental change in form handling:
useFormStatus - provides information about the form submission state in child componentsuseActionState - combines Server Actions with state management, validation, and progressive enhancementThese hooks eliminate much of the boilerplate associated with form handling - you no longer need manual
onSubmit, useState for each field, or a separate isLoading state. React 19 manages this automatically, like an autopilot on a spaceship.