9.5 KiB
9.5 KiB
React 19 Form Modernization Example
This document shows a practical example of how existing forms in our codebase could be simplified using React 19 features.
Example: Sign-In Form Modernization
Current Implementation (React 18 Pattern)
// Current approach using Redux, manual state management
function SignInComponent({ emailSignInStart, loginLoading, signInError }) {
const [form] = Form.useForm();
const handleFinish = (values) => {
const { email, password } = values;
emailSignInStart(email, password);
};
return (
<Form form={form} onFinish={handleFinish}>
<Form.Item name="email" rules={[{ required: true, type: 'email' }]}>
<Input prefix={<UserOutlined />} placeholder="Email" />
</Form.Item>
<Form.Item name="password" rules={[{ required: true }]}>
<Input.Password prefix={<LockOutlined />} placeholder="Password" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loginLoading} block>
{loginLoading ? 'Signing in...' : 'Sign In'}
</Button>
</Form.Item>
{signInError && <AlertComponent type="error" message={signInError} />}
</Form>
);
}
Characteristics:
- ✅ Works well with Ant Design
- ✅ Good separation with Redux
- ⚠️ Loading state managed in Redux
- ⚠️ Error state managed in Redux
- ⚠️ Multiple state slices for one operation
Modern Alternative (React 19 Pattern)
Option 1: Keep Ant Design + Add useActionState for cleaner Redux actions
import { useActionState } from 'react';
import { Form, Input, Button } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
function SignInModern() {
const [form] = Form.useForm();
// Wrap your Redux action with useActionState
const [state, submitAction, isPending] = useActionState(
async (prevState, formData) => {
try {
// Call your Redux action
await emailSignInAsync(
formData.get('email'),
formData.get('password')
);
return { error: null, success: true };
} catch (error) {
return { error: error.message, success: false };
}
},
{ error: null, success: false }
);
return (
<Form
form={form}
onFinish={(values) => {
// Convert Ant Design form values to FormData
const formData = new FormData();
formData.append('email', values.email);
formData.append('password', values.password);
submitAction(formData);
}}
>
<Form.Item name="email" rules={[{ required: true, type: 'email' }]}>
<Input prefix={<UserOutlined />} placeholder="Email" />
</Form.Item>
<Form.Item name="password" rules={[{ required: true }]}>
<Input.Password prefix={<LockOutlined />} placeholder="Password" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={isPending} block>
{isPending ? 'Signing in...' : 'Sign In'}
</Button>
</Form.Item>
{state.error && <AlertComponent type="error" message={state.error} />}
</Form>
);
}
Benefits:
- ✅ Loading state is local (no Redux slice needed)
- ✅ Error handling is simpler
- ✅ Still works with Ant Design validation
- ✅ Less Redux boilerplate
Option 2: Native HTML Form + React 19 (for simpler use cases)
import { useActionState } from 'react';
import { signInWithEmailAndPassword } from '@firebase/auth';
import { auth } from '../../firebase/firebase.utils';
function SimpleSignIn() {
const [state, formAction, isPending] = useActionState(
async (prevState, formData) => {
const email = formData.get('email');
const password = formData.get('password');
try {
await signInWithEmailAndPassword(auth, email, password);
return { error: null };
} catch (error) {
return { error: error.message };
}
},
{ error: null }
);
return (
<form action={formAction} className="sign-in-form">
<input
type="email"
name="email"
placeholder="Email"
required
/>
<input
type="password"
name="password"
placeholder="Password"
required
/>
<button type="submit" disabled={isPending}>
{isPending ? 'Signing in...' : 'Sign In'}
</button>
{state.error && <div className="error">{state.error}</div>}
</form>
);
}
Benefits:
- ✅ Minimal code
- ✅ No form library needed
- ✅ Built-in HTML5 validation
- ⚠️ Less feature-rich than Ant Design
Recommendation for Our Codebase
Keep Current Pattern When:
- Using complex Ant Design form features (nested forms, dynamic fields, etc.)
- Form state needs to be in Redux for other reasons
- Form is working well and doesn't need changes
Consider React 19 Pattern When:
- Creating new simple forms
- Form only needs local state
- Want to reduce Redux boilerplate
- Building optimistic UI features
Real-World Example: Job Note Adding
Let's look at a more practical example for our domain:
Adding Job Notes with Optimistic UI
import { useOptimistic, useActionState } from 'react';
import { Form, Input, Button, List } from 'antd';
function JobNotesModern({ jobId, initialNotes }) {
const [notes, setNotes] = useState(initialNotes);
// Optimistic UI for instant feedback
const [optimisticNotes, addOptimisticNote] = useOptimistic(
notes,
(currentNotes, newNote) => [newNote, ...currentNotes]
);
// Form submission with loading state
const [state, submitAction, isPending] = useActionState(
async (prevState, formData) => {
const noteText = formData.get('note');
// Show note immediately (optimistic)
const tempNote = {
id: `temp-${Date.now()}`,
text: noteText,
createdAt: new Date().toISOString(),
pending: true,
};
addOptimisticNote(tempNote);
try {
// Save to server
const response = await fetch(`/api/jobs/${jobId}/notes`, {
method: 'POST',
body: JSON.stringify({ text: noteText }),
});
const savedNote = await response.json();
// Update with real note
setNotes(prev => [savedNote, ...prev]);
return { error: null, success: true };
} catch (error) {
// Optimistic note will disappear on next render
return { error: error.message, success: false };
}
},
{ error: null, success: false }
);
return (
<div className="job-notes">
<Form onFinish={(values) => {
const formData = new FormData();
formData.append('note', values.note);
submitAction(formData);
}}>
<Form.Item name="note" rules={[{ required: true }]}>
<Input.TextArea
placeholder="Add a note..."
rows={3}
/>
</Form.Item>
<Button type="primary" htmlType="submit" loading={isPending}>
{isPending ? 'Adding...' : 'Add Note'}
</Button>
{state.error && <div className="error">{state.error}</div>}
</Form>
<List
dataSource={optimisticNotes}
renderItem={note => (
<List.Item style={{ opacity: note.pending ? 0.5 : 1 }}>
<List.Item.Meta
title={note.text}
description={new Date(note.createdAt).toLocaleString()}
/>
{note.pending && <span className="badge">Saving...</span>}
</List.Item>
)}
/>
</div>
);
}
User Experience:
- User types note and clicks "Add Note"
- Note appears instantly (optimistic)
- Note is grayed out with "Saving..." badge
- Once saved, note becomes solid and badge disappears
- If error, note disappears and error shows
Benefits:
- ⚡ Instant feedback (feels faster)
- 🎯 Clear visual indication of pending state
- ✅ Automatic error handling
- 🧹 Clean, readable code
Migration Checklist
When modernizing a form to React 19 patterns:
Step 1: Analyze Current Form
- Does it need Redux state? (Multi-component access?)
- How complex is the validation?
- Does it benefit from optimistic UI?
- Is it a good candidate for modernization?
Step 2: Choose Pattern
- Keep Ant Design + useActionState (complex forms)
- Native HTML + Actions (simple forms)
- Add useOptimistic (instant feedback needed)
Step 3: Implement
- Create new branch
- Update component
- Test loading states
- Test error states
- Test success flow
Step 4: Review
- Code is cleaner/simpler?
- No loss of functionality?
- Better UX?
- Team understands pattern?
Conclusion
React 19's new features are additive - they give us new tools without breaking existing patterns.
Recommended Approach:
- ✅ Keep current forms working as-is
- 🎯 Try React 19 patterns in NEW forms first
- 📚 Learn by doing in low-risk features
- 🔄 Gradually adopt where it makes sense
Don't:
- ❌ Rush to refactor everything
- ❌ Break working code
- ❌ Force patterns where they don't fit
Do:
- ✅ Experiment with new features
- ✅ Share learnings with team
- ✅ Use where it improves code
- ✅ Enjoy better DX (Developer Experience)!
Next Steps
- Review the main REACT_19_FEATURES_GUIDE.md
- Try
useActionStatein one new form - Share feedback with the team
- Consider optimistic UI for high-traffic features
Happy coding! 🚀