Commit Graph

184 Commits

Author SHA1 Message Date
Sebastian Markbåge
d6cfa0f295
[Fiber] Use Owner/JSX Stack When Appending Stacks to Console (#29206)
This one should be fully behind the `enableOwnerStacks` flag.

Instead of printing the parent Component stack all the way to the root,
this now prints the owner stack of every JSX callsite. It also includes
intermediate callsites between the Component and the JSX call so it has
potentially more frames. Mainly it provides the line number of the JSX
callsite. In terms of the number of components is a subset of the parent
component stack so it's less information in that regard. This is usually
better since it's more focused on components that might affect the
output but if it's contextual based on rendering it's still good to have
parent stack. Therefore, I still use the parent stack when printing DOM
nesting warnings but I plan on switching that format to a diff view
format instead (Next.js already reformats the parent stack like this).

__Follow ups__

- Server Components show up in the owner stack for client logs but logs
done by Server Components don't yet get their owner stack printed as
they're replayed. They're also not yet printed in the server logs of the
RSC server.

- Server Component stack frames are formatted as the server and added to
the end but this might be a different format than the browser. E.g. if
server is running V8 and browser is running JSC or vice versa. Ideally
we can reformat them in terms of the client formatting.

- This doesn't yet update Fizz or DevTools. Those will be follow ups.
Fizz still prints parent stacks in the server side logs. The stacks
added to user space `console.error` calls by DevTools still get the
parent stacks instead.

- It also doesn't yet expose these to user space so there's no way to
get them inside `onCaughtError` for example or inside a custom
`console.error` override.

- In another follow up I'll use `console.createTask` instead and
completely remove these stacks if it's available.
2024-05-25 11:58:17 -04:00
Andrew Clark
ee5c194930
Fix async batching in React.startTransition (#29226)
Note: Despite the similar-sounding description, this fix is unrelated to
the issue where updates that occur after an `await` in an async action
must also be wrapped in their own `startTransition`, due to the absence
of an AsyncContext mechanism in browsers today.

---

Discovered a flaw in the current implementation of the isomorphic
startTransition implementation (React.startTransition), related to async
actions. It only creates an async scope if something calls setState
within the synchronous part of the action (i.e. before the first
`await`). I had thought this was fine because if there's no update
during this part, then there's nothing that needs to be entangled. I
didn't think this through, though — if there are multiple async updates
interleaved throughout the rest of the action, we need the async scope
to have already been created so that _those_ are batched together.

An even easier way to observe this is to schedule an optimistic update
after an `await` — the optimistic update should not be reverted until
the async action is complete.

To implement, during the reconciler's module initialization, we compose
its startTransition implementation with any previous reconciler's
startTransition that was already initialized. Then, the isomorphic
startTransition is the composition of every
reconciler's startTransition.

```js
function startTransition(fn) {
  return startTransitionDOM(() => {
    return startTransitionART(() => {
      return startTransitionThreeFiber(() => {
        // and so on...
        return fn();
      });
    });
  });
}
```

This is basically how flushSync is implemented, too.
2024-05-23 17:19:09 -04:00
Andrew Clark
c3345638cb
Support useFormStatus in progressively-enhanced forms (#29019)
Before this change, `useFormStatus` is only activated if a form is
submitted by an action function (either `<form action={actionFn}>` or
`<button formAction={actionFn}>`).

After this change, `useFormStatus` will also be activated if you call
`startTransition(actionFn)` inside a submit event handler that is
`preventDefault`-ed.

This is the last missing piece for implementing a custom `action` prop
that is progressively enhanced using `onSubmit` while maintaining the
same behavior as built-in form actions.

Here's the basic recipe for implementing a progressively-enhanced form
action. This would typically be implemented in your UI component
library, not regular application code:

```js
import {requestFormReset} from 'react-dom';

// To implement progressive enhancement, pass both a form action *and* a
// submit event handler. The action is used for submissions that happen
// before hydration, and the submit handler is used for submissions that
// happen after.
<form
  action={action}
  onSubmit={(event) => {
    // After hydration, we upgrade the form with additional client-
    // only behavior.
    event.preventDefault();

    // Manually dispatch the action.
    startTransition(async () => {
      // (Optional) Reset any uncontrolled inputs once the action is
      // complete, like built-in form actions do.
      requestFormReset(event.target);

      // ...Do extra action-y stuff in here, like setting a custom
      // optimistic state...

      // Call the user-provided action
      const formData = new FormData(event.target);
      await action(formData);
    });
  }}
/>
```
2024-05-09 13:16:08 -04:00
Jan Kassens
ed71a3ad29
Support ref cleanup function for imperative handle refs (#28910)
Support ref cleanup function for imperative handle refs
2024-04-25 12:51:41 -04:00
Sebastian Markbåge
9f2eebd807
[Fiber/Fizz] Support AsyncIterable as Children and AsyncGenerator Client Components (#28868)
Stacked on #28849, #28854, #28853. Behind a flag.

If you're following along from the side-lines. This is probably not what
you think it is.

It's NOT a way to get updates to a component over time. The
AsyncIterable works like an Iterable already works in React which is how
an Array works. I.e. it's a list of children - not the value of a child
over time.

It also doesn't actually render one component at a time. The way it
works is more like awaiting the entire list to become an array and then
it shows up. Before that it suspends the parent.

To actually get these to display one at a time, you have to opt-in with
`<SuspenseList>` to describe how they should appear. That's really the
interesting part and that not implemented yet.

Additionally, since these are effectively Async Functions and uncached
promises, they're not actually fully "supported" on the client yet for
the same reason rendering plain Promises and Async Functions aren't.
They warn. It's only really useful when paired with RSC that produces
instrumented versions of these. Ideally we'd published instrumented
helpers to help with map/filter style operations that yield new
instrumented AsyncIterables.

The way the implementation works basically just relies on unwrapThenable
and otherwise works like a plain Iterator.

There is one quirk with these that are different than just promises. We
ask for a new iterator each time we rerender. This means that upon retry
we kick off another iteration which itself might kick off new requests
that block iterating further. To solve this and make it actually
efficient enough to use on the client we'd need to stash something like
a buffer of the previous iteration and maybe iterator on the iterable so
that we can continue where we left off or synchronously iterate if we've
seen it before. Similar to our `.value` convention on Promises.

In Fizz, I had to do a special case because when we render an iterator
child we don't actually rerender the parent again like we do in Fiber.
However, it's more efficient to just continue on where we left off by
reusing the entries from the thenable state from before in that case.
2024-04-22 13:25:05 -04:00
Andrew Clark
ea26e38e33
[Experiment] Reuse memo cache after interruption (#28878)
Adds an experimental feature flag to the implementation of useMemoCache,
the internal cache used by the React Compiler (Forget).

When enabled, instead of treating the cache as copy-on-write, like we do
with fibers, we share the same cache instance across all render
attempts, even if the component is interrupted before it commits.

If an update is interrupted, either because it suspended or because of
another update, we can reuse the memoized computations from the previous
attempt. We can do this because the React Compiler performs atomic
writes to the memo cache, i.e. it will not record the inputs to a
memoization without also recording its output.

This gives us a form of "resuming" within components and hooks.

This only works when updating a component that already mounted. It has
no impact during initial render, because the memo cache is stored on the
fiber, and since we have not implemented resuming for fibers, it's
always a fresh memo cache, anyway.

However, this alone is pretty useful — it happens whenever you update
the UI with fresh data after a mutation/action, which is extremely
common in a Suspense-driven (e.g. RSC or Relay) app.

So the impact of this feature is faster data mutations/actions (when the
React Compiler is used).
2024-04-19 19:30:01 -04:00
Andrew Clark
da69b6af96
ReactDOM.requestFormReset (#28809)
Based on:

- #28808
- #28804 

---

This adds a React DOM method called requestFormReset that schedules a
form reset to occur when the current transition completes.

Internally, it's the same method that's called automatically whenever a
form action is submitted. It only affects uncontrolled form inputs. See
https://github.com/facebook/react/pull/28804 for details.

The reason for the public API is so UI libraries can implement their own
action-based APIs and maintain the form-resetting behavior, something
like this:

```js
function onSubmit(event) {
  // Disable default form submission behavior
  event.preventDefault();
  const form = event.target;
  startTransition(async () => {
    // Request the form to reset once the action
    // has completed
    requestFormReset(form);

    // Call the user-provided action prop
    await action(new FormData(form));
  })
}
```
2024-04-10 16:56:55 -04:00
Andrew Clark
374b5d26c2
Scaffolding for requestFormReset API (#28808)
Based on:

- #28804 

---

This sets adds a new ReactDOM export called requestFormReset, including
setting up the export and creating a method on the internal ReactDOM
dispatcher. It does not yet add any implementation.

Doing this in its own commit for review purposes.

The API itself will be explained in the next PR.
2024-04-10 16:55:15 -04:00
Andrew Clark
41950d14a5
Automatically reset forms after action finishes (#28804)
This updates the behavior of form actions to automatically reset the
form's uncontrolled inputs after the action finishes.

This is a frequent feature request for people using actions and it
aligns the behavior of client-side form submissions more closely with
MPA form submissions.

It has no impact on controlled form inputs. It's the same as if you
called `form.reset()` manually, except React handles the timing of when
the reset happens, which is tricky/impossible to get exactly right in
userspace.

The reset shouldn't happen until the UI has updated with the result of
the action. So, resetting inside the action is too early.

Resetting in `useEffect` is better, but it's later than ideal because
any effects that run before it will observe the state of the form before
it's been reset.

It needs to happen in the mutation phase of the transition. More
specifically, after all the DOM mutations caused by the transition have
been applied. That way the `defaultValue` of the inputs are updated
before the values are reset. The idea is that the `defaultValue`
represents the current, canonical value sent by the server.

Note: this change has no effect on form submissions that aren't
triggered by an action.
2024-04-10 16:54:24 -04:00
Ricky
9644d206e8
Soften useFormState warning (#28788)
It's not deprecated, it's really just renamed. Let's make the warning
less scary.
2024-04-08 23:22:17 -04:00
Sebastian Markbåge
d50323eb84
Flatten ReactSharedInternals (#28783)
This is similar to #28771 but for isomorphic. We need a make over for
these dispatchers anyway so this is the first step. Also helps flush out
some internals usage that will break anyway.

It flattens the inner mutable objects onto the ReactSharedInternals.
2024-04-08 19:23:23 -04:00
Josh Story
8e1462e8c4
[Fiber] Move updatePriority tracking to renderers (#28751)
Currently updatePriority is tracked in the reconciler. `flushSync` is
going to be implemented reconciler agnostic soon and we need to move the
tracking of this state to the renderer and out of reconciler. This
change implements new renderer bin dings for getCurrentUpdatePriority
and setCurrentUpdatePriority.

I was originally going to have the getter also do the event priority
defaulting using window.event so we eliminate getCur rentEventPriority
but this makes all the callsites where we store the true current
updatePriority on the stack harder to work with so for now they remain
separate.

I also moved runWithPriority to the renderer since it really belongs
whereever the state is being managed and it is only currently exposed in
the DOM renderer.

Additionally the current update priority is not stored on
ReactDOMSharedInternals. While not particularly meaningful in this
change it opens the door to implementing `flushSync` outside of the
reconciler
2024-04-08 08:53:17 -07:00
Jan Kassens
20e710aeab
Cleanup enableUseRefAccessWarning flag (#28699)
Cleanup enableUseRefAccessWarning flag

I don't think this flag has a path forward in the current
implementation. The detection by stack trace is too brittle to detect
the lazy initialization pattern reliably (see e.g. some internal tests
that expect the warning because they use lazy intialization, but a
slightly different pattern then the expected pattern.

I think a new version of this could be to fully ban ref access during
render with an alternative API for the exceptional cases that today
require ref access during render.
2024-04-03 13:35:38 -04:00
Sebastian Markbåge
5de8703646
Use the disableLegacyMode where ever we check the ConcurrentMode mode (#28657)
Saves some bytes and ensures that we're actually disabling it.

Turns out this flag wasn't disabling React Native/Fabric, React Noop and
React ART legacy modes so those are updated too.

Should be rebased on #28681.
2024-04-02 21:07:28 -04:00
Ricky
18812b645c
Warn when using useFormState (#28668)
## Overview

useFormState has been replaced with useActionState. Warn when it's used.

Also removes the `experimental_useFormState` warnings.
2024-03-29 13:40:54 -04:00
Ricky
05797ccebd
s/form state/action state (#28631)
Rename internals from "form state" to "action state"
2024-03-28 11:34:24 -04:00
Jan Kassens
527ed72bfd
Cleanup enableFormActions flag (#28614)
Cleanup enableFormActions flag
2024-03-25 13:25:14 -04:00
Ricky
5c65b27587
Add React.useActionState (#28491)
## Overview

_Depends on https://github.com/facebook/react/pull/28514_

This PR adds a new React hook called `useActionState` to replace and
improve the ReactDOM `useFormState` hook.

## Motivation

This hook intends to fix some of the confusion and limitations of the
`useFormState` hook.

The `useFormState` hook is only exported from the `ReactDOM` package and
implies that it is used only for the state of `<form>` actions, similar
to `useFormStatus` (which is only for `<form>` element status). This
leads to understandable confusion about why `useFormState` does not
provide a `pending` state value like `useFormStatus` does.

The key insight is that the `useFormState` hook does not actually return
the state of any particular form at all. Instead, it returns the state
of the _action_ passed to the hook, wrapping it and returning a
trackable action to add to a form, and returning the last returned value
of the action given. In fact, `useFormState` doesn't need to be used in
a `<form>` at all.

Thus, adding a `pending` value to `useFormState` as-is would thus be
confusing because it would only return the pending state of the _action_
given, not the `<form>` the action is passed to. Even if we wanted to
tie them together, the returned `action` can be passed to multiple
forms, creating confusing and conflicting pending states during multiple
form submissions.

Additionally, since the action is not related to any particular
`<form>`, the hook can be used in any renderer - not only `react-dom`.
For example, React Native could use the hook to wrap an action, pass it
to a component that will unwrap it, and return the form result state and
pending state. It's renderer agnostic.

To fix these issues, this PR:
- Renames `useFormState` to `useActionState`
- Adds a `pending` state to the returned tuple
- Moves the hook to the `'react'` package

## Reference

The `useFormState` hook allows you to track the pending state and return
value of a function (called an "action"). The function passed can be a
plain JavaScript client function, or a bound server action to a
reference on the server. It accepts an optional `initialState` value
used for the initial render, and an optional `permalink` argument for
renderer specific pre-hydration handling (such as a URL to support
progressive hydration in `react-dom`).

Type:

```ts
function useActionState<State>(
        action: (state: Awaited<State>) => State | Promise<State>,
        initialState: Awaited<State>,
        permalink?: string,
    ): [state: Awaited<State>, dispatch: () => void, boolean];
```

The hook returns a tuple with:
- `state`: the last state the action returned
- `dispatch`: the method to call to dispatch the wrapped action
- `pending`: the pending state of the action and any state updates
contained

Notably, state updates inside of the action dispatched are wrapped in a
transition to keep the page responsive while the action is completing
and the UI is updated based on the result.

## Usage

The `useActionState` hook can be used similar to `useFormState`:

```js
import { useActionState } from "react"; // not react-dom

function Form({ formAction }) {
  const [state, action, isPending] = useActionState(formAction);

  return (
    <form action={action}>
      <input type="email" name="email" disabled={isPending} />
      <button type="submit" disabled={isPending}>
        Submit
      </button>
      {state.errorMessage && <p>{state.errorMessage}</p>}
    </form>
  );
}
```

But it doesn't need to be used with a `<form/>` (neither did
`useFormState`, hence the confusion):

```js
import { useActionState, useRef } from "react";

function Form({ someAction }) {
  const ref = useRef(null);
  const [state, action, isPending] = useActionState(someAction);

  async function handleSubmit() {
    // See caveats below
    await action({ email: ref.current.value });
  }

  return (
    <div>
      <input ref={ref} type="email" name="email" disabled={isPending} />
      <button onClick={handleSubmit} disabled={isPending}>
        Submit
      </button>
      {state.errorMessage && <p>{state.errorMessage}</p>}
    </div>
  );
}
```

## Benefits

One of the benefits of using this hook is the automatic tracking of the
return value and pending states of the wrapped function. For example,
the above example could be accomplished via:

```js
import { useActionState, useRef } from "react";

function Form({ someAction }) {
  const ref = useRef(null);
  const [state, setState] = useState(null);
  const [isPending, setIsPending] = useTransition();

  function handleSubmit() {
    startTransition(async () => {
      const response = await someAction({ email: ref.current.value });
      setState(response);
    });
  }

  return (
    <div>
      <input ref={ref} type="email" name="email" disabled={isPending} />
      <button onClick={handleSubmit} disabled={isPending}>
        Submit
      </button>
      {state.errorMessage && <p>{state.errorMessage}</p>}
    </div>
  );
}
```

However, this hook adds more benefits when used with render specific
elements like react-dom `<form>` elements and Server Action. With
`<form>` elements, React will automatically support replay actions on
the form if it is submitted before hydration has completed, providing a
form of partial progressive enhancement: enhancement for when javascript
is enabled but not ready.

Additionally, with the `permalink` argument and Server Actions,
frameworks can provide full progressive enhancement support, submitting
the form to the URL provided along with the FormData from the form. On
submission, the Server Action will be called during the MPA navigation,
similar to any raw HTML app, server rendered, and the result returned to
the client without any JavaScript on the client.

## Caveats
There are a few Caveats to this new hook:
**Additional state update**: Since we cannot know whether you use the
pending state value returned by the hook, the hook will always set the
`isPending` state at the beginning of the first chained action,
resulting in an additional state update similar to `useTransition`. In
the future a type-aware compiler could optimize this for when the
pending state is not accessed.

**Pending state is for the action, not the handler**: The difference is
subtle but important, the pending state begins when the return action is
dispatched and will revert back after all actions and transitions have
settled. The mechanism for this under the hook is the same as
useOptimisitic.

Concretely, what this means is that the pending state of
`useActionState` will not represent any actions or sync work performed
before dispatching the action returned by `useActionState`. Hopefully
this is obvious based on the name and shape of the API, but there may be
some temporary confusion.

As an example, let's take the above example and await another action
inside of it:

```js
import { useActionState, useRef } from "react";

function Form({ someAction, someOtherAction }) {
  const ref = useRef(null);
  const [state, action, isPending] = useActionState(someAction);

  async function handleSubmit() {
    await someOtherAction();

    // The pending state does not start until this call.
    await action({ email: ref.current.value });
  }

  return (
    <div>
      <input ref={ref} type="email" name="email" disabled={isPending} />
      <button onClick={handleSubmit} disabled={isPending}>
        Submit
      </button>
      {state.errorMessage && <p>{state.errorMessage}</p>}
    </div>
  );
}

```

Since the pending state is related to the action, and not the handler or
form it's attached to, the pending state only changes when the action is
dispatched. To solve, there are two options.

First (recommended): place the other function call inside of the action
passed to `useActionState`:

```js
import { useActionState, useRef } from "react";

function Form({ someAction, someOtherAction }) {
  const ref = useRef(null);
  const [state, action, isPending] = useActionState(async (data) => {
    // Pending state is true already.
    await someOtherAction();
    return someAction(data);
  });

  async function handleSubmit() {
    // The pending state starts at this call.
    await action({ email: ref.current.value });
  }

  return (
    <div>
      <input ref={ref} type="email" name="email" disabled={isPending} />
      <button onClick={handleSubmit} disabled={isPending}>
        Submit
      </button>
      {state.errorMessage && <p>{state.errorMessage}</p>}
    </div>
  );
}
```

For greater control, you can also wrap both in a transition and use the
`isPending` state of the transition:

```js
import { useActionState, useTransition, useRef } from "react";

function Form({ someAction, someOtherAction }) {
  const ref = useRef(null);

  // isPending is used from the transition wrapping both action calls.
  const [isPending, startTransition] = useTransition();

  // isPending not used from the individual action.
  const [state, action] = useActionState(someAction);

  async function handleSubmit() {
    startTransition(async () => {
      // The transition pending state has begun.
      await someOtherAction();
      await action({ email: ref.current.value });
    });
  }

  return (
    <div>
      <input ref={ref} type="email" name="email" disabled={isPending} />
      <button onClick={handleSubmit} disabled={isPending}>
        Submit
      </button>
      {state.errorMessage && <p>{state.errorMessage}</p>}
    </div>
  );
}
```

A similar technique using `useOptimistic` is preferred over using
`useTransition` directly, and is left as an exercise to the reader.

## Thanks

Thanks to @ryanflorence @mjackson @wesbos
(https://github.com/facebook/react/issues/27980#issuecomment-1960685940)
and [Allan
Lasser](https://allanlasser.com/posts/2024-01-26-avoid-using-reacts-useformstatus)
for their feedback and suggestions on `useFormStatus` hook.
2024-03-22 13:03:44 -04:00
Sebastian Silbermann
8ef14cf242
Ensure dispatch from useFormState works in StrictMode (#28557)
Co-authored-by: Andrew Clark <git@andrewclark.io>
2024-03-20 16:15:22 +01:00
Rom Grk
4d686a2da1
Performance: avoid triggering map deopt in V8 (#28569)
The shape of the objects changed by this PR are both created in 2
locations with 2 different shapes, which most JS engines won't like.
I've noticed this in particular in V8 while benchmarking production
code.


1293047d60/packages/react-reconciler/src/ReactFiberCacheComponent.js (L66-L77)


1293047d60/packages/react-reconciler/src/ReactFiberHostContext.js (L47-L54)


1293047d60/packages/react-reconciler/src/ReactFiberHooks.js (L3530-L3531)


1293047d60/packages/react-reconciler/src/ReactFiberHooks.js (L3492-L3493)
2024-03-18 11:54:34 -04:00
Ricky
17eaacaac1
Add pending state to useFormState (#28514)
## Overview

Adds a `pending` state to useFormState, which will be replaced by
`useActionState` in the next diff. We will keep `useFormState` around
for backwards compatibility, but functionally it will work the same as
`useActionState`, which has an `isPending` state returned.
2024-03-12 15:50:11 -04:00
Ricky
1940cb27b2
Update /link URLs to react.dev (#28477)
Depends on https://github.com/reactjs/react.dev/pull/6670 [merged]
2024-03-03 17:34:33 -05:00
Sebastian Silbermann
97fd3e7064
Ensure useState and useReducer initializer functions are double invoked in StrictMode (#28248) 2024-02-06 17:53:08 +01:00
Sathya Gunasekaran
db120f69ec
Patch devtools before running useMemo function in strict mode (#28249)
This fixes a regression https://github.com/facebook/react/pull/25583
where we stopped patching before calling useMemo function.

Fixes https://github.com/facebook/react/issues/27989
2024-02-06 16:45:18 +00:00
dan
472854820b
[Flight] Delete Server Context (#28225)
Server Context was never documented, and has been deprecated in
https://github.com/facebook/react/pull/27424.

This PR removes it completely, including the implementation code.

Notably, `useContext` is removed from the shared subset, so importing it
from a React Server environment would now should be a build error in
environments that are able to enforce that.
2024-02-05 22:39:15 +00:00
Andrew Clark
178f435194
Always warn if client component suspends with an uncached promise (#28159)
Previously we only warned during a synchronous update, because we
eventually want to support async client components in controlled
scenarios, like during navigations. However, we're going to warn in all
cases for now until we figure out how that should work.
2024-01-30 14:23:39 -05:00
Andrew Clark
60f190a559
Capture React.startTransition errors and pass to reportError (#28111)
To make React.startTransition more consistent with the hook form of
startTransition, we capture errors thrown by the scope function and pass
them to the global reportError function. (This is also what we do as a
default for onRecoverableError.)

This is a breaking change because it means that errors inside of
startTransition will no longer bubble up to the caller. You can still
catch the error by putting a try/catch block inside of the scope
function itself.

We do the same for async actions to prevent "unhandled promise
rejection" warnings.

The motivation is to avoid a refactor hazard when changing from a sync
to an async action, or from useTransition to startTransition.
2024-01-26 12:10:33 -05:00
Andrew Clark
85b296e9b6
Async action support for React.startTransition (#28097)
This adds support for async actions to the "isomorphic" version of
startTransition (i.e. the one exported by the "react" package).
Previously, async actions were only supported by the startTransition
that is returned from the useTransition hook.

The interesting part about the isomorphic startTransition is that it's
not associated with any particular root. It must work with updates to
arbitrary roots, or even arbitrary React renderers in the same app. (For
example, both React DOM and React Three Fiber.)

The idea is that React.startTransition should behave as if every root
had an implicit useTransition hook, and you composed together all the
startTransitions provided by those hooks. Multiple updates to the same
root will be batched together. However, updates to one root will not be
batched with updates to other roots.

Features like useOptimistic work the same as with the hook version.

There is one difference from from the hook version of startTransition:
an error triggered inside an async action cannot be captured by an error
boundary, because it's not associated with any particular part of the
tree. You should handle errors the same way you would in a regular
event, e.g. with a global error event handler, or with a local
`try/catch`.
2024-01-25 21:54:45 -05:00
Andrew Clark
11c9fd0c53
Batch async actions even if useTransition is unmounted (#28078)
If there are multiple updates inside an async action, they should all be
rendered in the same batch, even if they are separate by an async
operation (`await`). We currently implement this by suspending in the
`useTransition` hook to block the update from committing until all
possible updates have been scheduled by the action. The reason we did it
this way is so you can "cancel" an action by navigating away from the UI
that triggered it.

The problem with that approach, though, is that even if you navigate
away from the `useTransition` hook, the action may have updated shared
parts of the UI that are still in the tree. So we may need to continue
suspending even after the `useTransition` hook is deleted.

In other words, the lifetime of an async action scope is longer than the
lifetime of a particular `useTransition` hook.

The solution is to suspend whenever _any_ update that is part of the
async action scope is unwrapped during render. So, inside useState and
useReducer.

This fixes a related issue where an optimistic update is reverted before
the async action has finished, because we were relying on the
`useTransition` hook to prevent the optimistic update from finishing.

This also prepares us to support async actions being passed to the
non-hook form of `startTransition` (though this isn't implemented yet).
2024-01-24 23:54:53 -05:00
Andrew Clark
60a927d04a
Fix: useOptimistic should return passthrough value when there are no updates pending (#27936)
This fixes a bug that happened when the canonical value passed to
useOptimistic without an accompanying call to setOptimistic. In this
scenario, useOptimistic should pass through the new canonical value.

I had written tests for the more complicated scenario, where a new value
is passed while there are still pending optimistic updates, but not this
simpler one.
2024-01-13 21:37:35 -05:00
Andrew Clark
77c4ac2ce8
[useFormState] Allow sync actions (#27571)
Updates useFormState to allow a sync function to be passed as an action.

A form action is almost always async, because it needs to talk to the
server. But since we support client-side actions, too, there's no reason
we can't allow sync actions, too.

I originally chose not to allow them to keep the implementation simpler
but it's not really that much more complicated because we already
support this for actions passed to startTransition. So now it's
consistent: anywhere an action is accepted, a sync client function is a
valid input.
2023-10-31 23:32:31 -04:00
Andrew Clark
b8e47d988e
Bugfix: useFormState queues actions in wrong order (#27570)
I neglected to update the "last" pointer of the action queue. Since the
queue is circular, rather than dropping the update, the effect was to
add the update to the front of the queue instead of the back. I didn't
notice earlier because in my demos/tests, the actions would either
resolve really quickly or the actions weren't order dependent (like
incrementing a counter).
2023-10-23 17:52:15 -04:00
Andrew Clark
75c1bd7ee7
Prerendering support for useDeferredValue (#27512)
### Based on #27509 

Revealing a prerendered tree (hidden -> visible) is considered the same
as mounting a brand new tree. So, when an initialValue argument is
passed to useDeferredValue, and it's prerendered inside a hidden tree,
we should first prerender the initial value.

After the initial value has been prerendered, we switch to prerendering
the final one. This is the same sequence that we use when mounting new
visible tree. Depending on how much prerendering work has been finished
by the time the tree is revealed, we may or may not be able to skip all
the way to the final value.

This means we get the benefits of both prerendering and preview states:
if we have enough resources to prerender the whole thing, we do that. If
we don't, we have a preview state to show for immediate feedback.
2023-10-17 12:58:48 -04:00
Andrew Clark
b2a68a65c8
useDeferredValue should skip initialValue if it suspends (#27509)
### Based on https://github.com/facebook/react/pull/27505

If a parent render spawns a deferred task with useDeferredValue, but the
parent render suspends, we should not wait for the parent render to
complete before attempting to render the final value.

The reason is that the initialValue argument to useDeferredValue is
meant to represent an immediate preview of the final UI. If we can't
render it "immediately", we might as well skip it and go straight to the
"real" value.

This is an improvement over how a userspace implementation of
useDeferredValue would work, because a userspace implementation would
have to wait for the parent task to commit (useEffect) before spawning
the deferred task, creating a waterfall.
2023-10-17 12:48:11 -04:00
Andrew Clark
309c8ad968
Track entangled lanes separately from update lane (#27505)
A small refactor to how the lane entanglement mechanism works. We can
now distinguish between the lane that "spawned" a render task (i.e. a
new update) versus the lanes that it's entangled with. Both the update
lane and the entangled lanes will be included while rendering, but by
keeping them separate, we don't lose the original priority.

In practical terms, this means we can now entangle a low priority update
with a higher priority lane while rendering at the lower priority.

To do this, lanes that are entangled at the root are now tracked using
the same variable that we use to track the "base lanes" when revealing a
previously hidden tree — conceptually, they are the same thing. I also
renamed this variable (from subtreeLanes to entangledRenderLanes) to
better reflect how it's used.

My primary motivation is related to useDeferredValue, which I'll address
in a later PR.
2023-10-15 12:29:52 -04:00
Andrew Clark
be67db46b6
Add optional initialValue argument to useDeferredValue (#27500)
Adds a second argument to useDeferredValue called initialValue:

```js
const value = useDeferredValue(finalValue, initialValue);
```

During the initial render of a component, useDeferredValue will return
initialValue. Once that render finishes, it will spawn an additional
render to switch to finalValue.

This same sequence should occur whenever the hook is hidden and revealed
again, i.e. by a Suspense or Activity, though this part is not yet
implemented.

When initialValue is not provided, useDeferredValue has no effect during
initial render, but during an update, it will remain on the previous
value, then spawn an additional render to switch to the new value. (This
is the same behavior that exists today.)

During SSR, initialValue is always used, if provided.

This feature is currently behind an experimental flag. We plan to ship
it in a non-breaking release.
2023-10-10 16:39:02 -04:00
Andrew Clark
88d56b8e81
Warn if optimistic state is updated outside of a transition (#27454)
### Based on #27453 

If optimistic state is updated, and there's no startTransition on the
stack, there are two likely scenarios.

One possibility is that the optimistic update is triggered by a regular
event handler (e.g. `onSubmit`) instead of an action. This is a mistake
and we will warn.

The other possibility is the optimistic update is inside an async
action, but after an `await`. In this case, we can make it "just work"
by associating the optimistic update with the pending async action.

Technically it's possible that the optimistic update is unrelated to the
pending action, but we don't have a way of knowing this for sure because
browsers currently do not provide a way to track async scope. (The
AsyncContext proposal, if it lands, will solve this in the future.)
However, this is no different than the problem of unrelated transitions
being grouped together — it's not wrong per se, but it's not ideal.

Once AsyncContext starts landing in browsers, we will provide better
warnings in development for these cases.
2023-10-04 10:57:55 -04:00
Andrew Clark
85c2b519b5
Fix: Optimistic update does not get reset (#27453)
I found a bug where if an optimistic update causes a component to
rerender, and there are no other state updates during that render, React
bails out without applying the update.

Whenever a hook detects a change, we must mark the component as dirty to
prevent a bailout. We check for changes by comparing the new state to
`hook.memoizedState`. However, when implementing optimistic state
rebasing, I incorrectly reset `hook.memoizedState` to the incoming base
state, even though I only needed to reset `hook.baseState`. This was
just a mistake on my part.

This wasn't caught by the existing tests because usually when the
optimistic state changes, there's also some other state that marks the
component as dirty in the same render.

I fixed the bug and added a regression test.
2023-10-03 15:01:40 -04:00
Andrew Clark
d6dcad6a8b
useFormState: Only emit markers if postback state is provided (#27374)
This is an optimization where useFormState will only emit extra comment
markers if a form state is passed at the root. If no state is passed, we
don't need to emit anything because none of the hooks will match.
2023-09-14 11:46:16 -04:00
Andrew Clark
612b2b6601
useFormState: Reuse state from previous form submission (#27321)
If a Server Action is passed to useFormState, the action may be
submitted before it has hydrated. This will trigger a full page
(MPA-style) navigation. We can transfer the form state to the next page
by comparing the key path of the hook instance.

`ReactServerDOMServer.decodeFormState` is used by the server to extract
the form state from the submitted action. This value can then be passed
as an option when rendering the new page. It must be passed during both
SSR and hydration.

```js
const boundAction = await decodeAction(formData, serverManifest);
const result = await boundAction();
const formState = decodeFormState(result, formData, serverManifest);

// SSR
const response = createFromReadableStream(<App />);
const ssrStream = await renderToReadableStream(response, { formState })

// Hydration
hydrateRoot(container, <App />, { formState });
```

If the `formState` option is omitted, then the state won't be
transferred to the next page. However, it must be passed in both places,
or in neither; misconfiguring will result in a hydration mismatch.

(The `formState` option is currently prefixed with `experimental_`)
2023-09-13 18:30:40 -04:00
Andrew Clark
8b26f07a88
useFormState: Emit comment to mark whether state matches (#27307)
A planned feature of useFormState is that if the page load is the result
of an MPA-style form submission — i.e. a form was submitted before it
was hydrated, using Server Actions — the state of the hook should
transfer to the next page.

I haven't implemented that part yet, but as a prerequisite, we need some
way for Fizz to indicate whether a useFormState hook was rendered using
the "postback" state. That way we can do all state matching logic on the
server without having to replicate it on the client, too.

The approach here is to emit a comment node for each useFormState hook.
We use one of two comment types: `<!--F-->` for a normal useFormState
hook, and `<!--F!-->` for a hook that was rendered using the postback
state. React will read these markers during hydration. This is similar
to how we encode Suspense boundaries.

Again, the actual matching algorithm is not yet implemented — for now,
the "not matching" marker is always emitted.

We can optimize this further by not emitting any markers for a render
that is not the result of a form postback, which I'll do in subsequent
PRs.
2023-09-07 16:05:44 -04:00
Andrew Clark
ddff504695
useFormState's permalink option changes form target (#27302)
When the `permalink` option is passed to `useFormState`, and the form is
submitted before it has hydrated, the permalink will be used as the
target of the form action, enabling MPA-style form submissions.

(Note that submitting a form without hydration is a feature of Server
Actions; it doesn't work with regular client actions.)

It does not have any effect after the form has hydrated.
2023-08-29 11:58:44 -04:00
Andrew Clark
456d153bb5
Client implementation of useFormState (#27278)
This implements useFormState in Fiber. (It does not include any
progressive enhancement features; those will be added later.)

useFormState is a hook for tracking state produced by async actions. It
has a signature similar to useReducer, but instead of a reducer, it
accepts an async action function.

```js
async function action(prevState, payload) {
  // ..
}
const [state, dispatch] = useFormState(action, initialState)
```

Calling dispatch runs the async action and updates the state to the
returned value.

Async actions run before React's render cycle, so unlike reducers, they
can contain arbitrary side effects.
2023-08-28 11:05:40 -04:00
Andrew Clark
9a01c8b54e
Fix mount-or-update check in rerenderOptimistic (#27277)
I noticed this was wrong because it should call updateWorkInProgressHook
before it checks if currentHook is null.
2023-08-28 11:04:46 -04:00
Andrew Clark
b4cdd3e892
Scaffolding for useFormState (#27270)
This exposes, but does not yet implement, a new experimental API called
useFormState. It's gated behind the enableAsyncActions flag.

useFormState has a similar signature to useReducer, except instead of a
reducer it accepts an (async) action function. React will wait until the
promise resolves before updating the state:

```js
async function action(prevState, payload) {
  // ..
}
const [state, dispatch] = useFormState(action, initialState)
```

When used in combination with Server Actions, it will also support
progressive enhancement — a form that is submitted before it has
hydrated will have its state transferred to the next page. However, like
the other action-related hooks, it works with fully client-driven
actions, too.
2023-08-23 10:58:09 -04:00
Andrew Clark
5c8dabf886
Detect and warn about native async function components in development (#27031)
Adds a development warning to complement the error introduced by
https://github.com/facebook/react/pull/27019.

We can detect and warn about async client components by checking the
prototype of the function. This won't work for environments where async
functions are transpiled, but for native async functions, it allows us
to log an earlier warning during development, including in cases that
don't trigger the infinite loop guard added in
https://github.com/facebook/react/pull/27019. It does not supersede the
infinite loop guard, though, because that mechanism also prevents the
app from crashing.

I also added a warning for calling a hook inside an async function. This
one fires even during a transition. We could add a corresponding warning
to Flight, since hooks are not allowed in async Server Components,
either. (Though in both environments, this is better handled by a lint
rule.)
2023-07-01 22:44:41 -04:00
Noah Lemen
80d9a40114
Remove useMutableSource (#27011)
## Summary

This PR cleans up `useMutableSource`. This has been blocked by a
remaining dependency internally at Meta, but that has now been deleted.

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

## How did you test this change?

```
yarn flow
yarn lint
yarn test --prod
```

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->
2023-06-27 12:45:46 -04:00
Tianyu Yao
254cbdbd6d
Add a temporary internal option to disable double useEffect in legacy strict mode (#26914)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->
We are upgrading React 17 codebase to React18, and `StrictMode` has been
great for surfacing potential production bugs on React18 for class
components. There are non-trivial number of test failures caused by
double `useEffect` in StrictMode. To prioritize surfacing and fixing
issues that will break in production now, we need a flag to turn off
double `useEffect` for now in StrictMode temporarily. This is a
Meta-only hack for rolling out `createRoot` and we will fast follow to
remove it and use full strict mode.

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->
jest
2023-06-21 11:14:57 -07:00
Sophie Alpert
4cd7065665
Fix uSES hydration in strict mode (#26791)
Previously, we'd call and use getSnapshot on the second render resulting
in `Warning: Text content did not match. Server: "Nay!" Client: "Yay!"`
and then `Error: Text content does not match server-rendered HTML.`.

Fixes #26095. Closes #26113. Closes #25650.

---------

Co-authored-by: eps1lon <silbermann.sebastian@gmail.com>
2023-05-12 14:18:03 -07:00
Andrew Clark
b7972822b5
useOptimisticState -> useOptimistic (#26772)
Drop the "state". Just "useOptimistic". It's cleaner.

This is still an experimental API. May not be the final name.
2023-05-03 14:26:00 -04:00