Handling API Calls with useActionState in React

React 18.2 introduced a powerful hook called useActionState, designed to simplify form handling and async state transitions. While it’s commonly used to manage form submissions, one of its most practical applications is handling API calls.

In traditional React applications, managing form submissions and API requests often involves juggling multiple state variables like loading, error, and success using useState or useReducer. But with useActionState, React offers a more declarative and cleaner solution—especially when the form is the source of your user interaction.

In this blog, we’ll explore how to handle API calls effectively using useActionState in React. We’ll look at structuring API logic, validating input, managing success and error states, and building a better user experience.


Why Use useActionState for API Calls?

The main reason is simplicity. useActionState ties your form and its async behavior into one cohesive unit. Instead of manually handling onSubmit, preventing default behavior, calling your API, updating state, and rendering messages, you let the form trigger everything declaratively.

Some advantages of using useActionState for API calls:

  • It handles async form submissions in one place.
  • It provides a reducer-like function that runs after each form submission.
  • It returns updated state, which you can use to display errors or success messages.
  • It keeps your component code clean and declarative.

Basic useActionState API Call Example

Let’s start with a common scenario: subscribing a user to a newsletter via an API call.

Step 1: Define the state structure

type APIState = {
  loading: boolean;
  error: string | null;
  success: string | null;
};

Step 2: Setup the initial state

const initialState: APIState = {
  loading: false,
  error: null,
  success: null,
};

Step 3: Create the reducer function

This function handles form submission, validates the input, sends the API request, and returns new state.

const handleSubmit = async (
  prevState: APIState,
  formData: FormData
): Promise<APIState> => {
  const email = formData.get('email');

  if (!email || typeof email !== 'string' || !email.includes('@')) {
    return {
      loading: false,
      error: 'Please enter a valid email address.',
      success: null,
    };
  }

  try {
    const response = await fetch('https://api.example.com/subscribe', {
      method: 'POST',
      body: JSON.stringify({ email }),
      headers: {
        'Content-Type': 'application/json',
      },
    });

    if (!response.ok) {
      const result = await response.json();
      return {
        loading: false,
        error: result.message || 'Subscription failed.',
        success: null,
      };
    }

    return {
      loading: false,
      error: null,
      success: 'Successfully subscribed!',
    };
  } catch (error) {
    return {
      loading: false,
      error: 'Network error. Please try again later.',
      success: null,
    };
  }
};

Step 4: Use the hook inside your component

'use client';

import { useActionState } from 'react';

export default function NewsletterForm() {
  const [state, formAction] = useActionState(handleSubmit, initialState);

  return (
    <form action={formAction}>
      <input
        type="email"
        name="email"
        placeholder="Enter your email"
        required
      />
      <button type="submit" disabled={state.loading}>
        {state.loading ? 'Submitting...' : 'Subscribe'}
      </button>

      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}
      {state.success && <p style={{ color: 'green' }}>{state.success}</p>}
    </form>
  );
}

Tips for Handling API Calls Effectively with useActionState

1. Always validate input before the API call

Validating form values at the top of your reducer function prevents unnecessary network requests and gives users immediate feedback.

2. Keep your reducer pure and focused

Avoid side effects or complex UI logic inside the reducer. Use it only for validation, calling APIs, and returning state. Complex side effects (like redirects or global state updates) should be handled separately.

3. Avoid deeply nested or unnecessary state

Only store what you need: loading, success, error, or result data. Don’t store form values persistently unless required.

type State = {
  loading: boolean;
  error: string | null;
  result: any; // optional, only if API returns something useful
};

4. Disable form while submitting

Prevent duplicate API requests by disabling the submit button while the reducer is processing.

5. Extract API logic into reusable functions

For better code organization, separate API calls into utility functions or services.

async function subscribeUser(email: string) {
  const response = await fetch('/api/subscribe', {
    method: 'POST',
    body: JSON.stringify({ email }),
    headers: { 'Content-Type': 'application/json' },
  });
  return await response.json();
}

Then call subscribeUser from within your reducer.

6. Handle server errors gracefully

Not all API failures are network errors. Parse JSON responses and check for status codes to inform the user accurately.

7. Use TypeScript for safer form and API interactions

Strongly type the reducer arguments and return types to avoid bugs.


Advanced Example: Login Form with Token Handling

Let’s look at a more advanced example: a login form that sends credentials to an API and stores the token in localStorage.

type LoginState = {
  loading: boolean;
  error: string | null;
  success: string | null;
};

const loginReducer = async (
  prev: LoginState,
  formData: FormData
): Promise<LoginState> => {
  const email = formData.get('email');
  const password = formData.get('password');

  if (!email || !password || typeof email !== 'string' || typeof password !== 'string') {
    return {
      loading: false,
      error: 'Email and password are required.',
      success: null,
    };
  }

  try {
    const res = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password }),
      headers: { 'Content-Type': 'application/json' },
    });

    if (!res.ok) {
      const err = await res.json();
      return {
        loading: false,
        error: err.message || 'Login failed.',
        success: null,
      };
    }

    const data = await res.json();
    localStorage.setItem('token', data.token);

    return {
      loading: false,
      error: null,
      success: 'Logged in successfully.',
    };
  } catch {
    return {
      loading: false,
      error: 'Something went wrong.',
      success: null,
    };
  }
};

Use it in the component like this:

const initialLoginState = { loading: false, error: null, success: null };
const [state, formAction] = useActionState(loginReducer, initialLoginState);

return (
  <form action={formAction}>
    <input name="email" type="email" />
    <input name="password" type="password" />
    <button type="submit" disabled={state.loading}>
      {state.loading ? 'Logging in...' : 'Login'}
    </button>
    {state.error && <p>{state.error}</p>}
    {state.success && <p>{state.success}</p>}
  </form>
);

When to Avoid useActionState for API Calls

Although useActionState is excellent for form submissions tied to user input, it may not be ideal in the following cases:

  • When you need to fetch data on component mount (use useEffect instead)
  • When your API calls are unrelated to form submissions
  • For real-time data updates like websockets or polling
  • For optimistic UI updates where instant feedback is needed

Final Thoughts

The useActionState hook is a game-changer for managing form-driven API calls in React. It simplifies the logic, avoids excessive state management, and provides a clean structure for handling async operations. Whether you’re building login forms, contact forms, or multi-step workflows, useActionState makes managing API requests more maintainable and declarative.

As React continues to evolve toward server-centric and declarative patterns, mastering hooks like useActionState prepares you to build faster, cleaner, and more future-proof React applications.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top