Files
bodyshop/_reference/REACT_19_MODERNIZATION_EXAMPLES.md

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:

  1. Using complex Ant Design form features (nested forms, dynamic fields, etc.)
  2. Form state needs to be in Redux for other reasons
  3. Form is working well and doesn't need changes

Consider React 19 Pattern When:

  1. Creating new simple forms
  2. Form only needs local state
  3. Want to reduce Redux boilerplate
  4. 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:

  1. User types note and clicks "Add Note"
  2. Note appears instantly (optimistic)
  3. Note is grayed out with "Saving..." badge
  4. Once saved, note becomes solid and badge disappears
  5. 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:

  1. Keep current forms working as-is
  2. 🎯 Try React 19 patterns in NEW forms first
  3. 📚 Learn by doing in low-risk features
  4. 🔄 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

  1. Review the main REACT_19_FEATURES_GUIDE.md
  2. Try useActionState in one new form
  3. Share feedback with the team
  4. Consider optimistic UI for high-traffic features

Happy coding! 🚀