Repository: facebook/react
File: packages/react-dom-bindings/src/shared/ReactDOMFormActions.js (JavaScript)

Code before:
export function useFormStatus(): FormStatus {
  if (!(enableFormActions && enableAsyncActions)) {
    throw new Error('Not implemented.');
  } else {
    const dispatcher = resolveDispatcher();
    // $FlowFixMe[not-a-function] We know this exists because of the feature check above.
    return dispatcher.useHostTransitionStatus();
  }
}

export function useFormState<S, P>(
  action: (Awaited<S>, P) => S,
  initialState: Awaited<S>,
  permalink?: string,
): [Awaited<S>, (P) => void] {
  if (!(enableFormActions && enableAsyncActions)) {
    throw new Error('Not implemented.');
  } else {
    const dispatcher = resolveDispatcher();
    // $FlowFixMe[not-a-function] This is unstable, thus optional
    const [state, dispatch] = dispatcher.useActionState(
      action,
      initialState,
      permalink,
    );
    return [state, dispatch];
  }
}


Review comment (suggestion):
As you can see, I basically replaced all of the internals of `useFormState` with `useActionState` and just dispatch `useActionState` from `useFormState`. 

This means `useFormState` will have the additional `isPending` state update that `useActionState` will have, which seems fine since you need to migrate to it anyway, but if that's an issue I can add the `useFormState` dispatcher back and form the implementation a bit.

This also may create some confusion in DevTools since the name will be `useActionState` but seems fine since it was never released beyond canary and switching to the correct API fixes it, but again, let me know if that's a problem worth solving.

Code after:
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
import type {Awaited} from 'shared/ReactTypes';

import {enableAsyncActions, enableFormActions} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';

const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;

type FormStatusNotPending = {|
  pending: false,
  data: null,
  method: null,
  action: null,
|};

type FormStatusPending = {|
  pending: true,
  data: FormData,
  method: string,
  action: string | (FormData => void | Promise<void>),
|};

export type FormStatus = FormStatusPending | FormStatusNotPending;

// Since the "not pending" value is always the same, we can reuse the
// same object across all transitions.
const sharedNotPendingObject = {
  pending: false,
  data: null,
  method: null,
  action: null,
};

export const NotPending: FormStatus = __DEV__
  ? Object.freeze(sharedNotPendingObject)
  : sharedNotPendingObject;

function resolveDispatcher() {
  // Copied from react/src/ReactHooks.js. It's the same thing but in a
  // different package.
  const dispatcher = ReactCurrentDispatcher.current;
  if (__DEV__) {
    if (dispatcher === null) {
      console.error(
        'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
          ' one of the following reasons:\n' +
          '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
          '2. You might be breaking the Rules of Hooks\n' +
          '3. You might have more than one copy of React in the same app\n' +
          'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
      );
    }
  }
  // Will result in a null access error if accessed outside render phase. We
  // intentionally don't throw our own error because this is in a hot path.
  // Also helps ensure this is inlined.
  return ((dispatcher: any): Dispatcher);
}

export function useFormStatus(): FormStatus {
  if (!(enableFormActions && enableAsyncActions)) {
    throw new Error('Not implemented.');
  } else {
    const dispatcher = resolveDispatcher();
    // $FlowFixMe[not-a-function] We know this exists because of the feature check above.
    return dispatcher.useHostTransitionStatus();
  }
}

export function useFormState<S, P>(
  action: (Awaited<S>, P) => S,
  initialState: Awaited<S>,
  permalink?: string,
): [Awaited<S>, (P) => void, boolean] {
  if (!(enableFormActions && enableAsyncActions)) {
    throw new Error('Not implemented.');
  } else {
    const dispatcher = resolveDispatcher();
    // $FlowFixMe[not-a-function] This is unstable, thus optional
    return dispatcher.useFormState(action, initialState, permalink);
  }
}