Skip to content

Commit

Permalink
Client implementation of useFormState (#27278)
Browse files Browse the repository at this point in the history
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.

DiffTrain build for [456d153](456d153)
  • Loading branch information
acdlite committed Aug 28, 2023
1 parent a7a6a73 commit eeb3579
Show file tree
Hide file tree
Showing 21 changed files with 2,982 additions and 2,665 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
9a01c8b54e91c588391070c65701342e4e361ea7
456d153bb582798effa76c09bec2405ab2e392cf
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ if (
}
"use strict";

var ReactVersion = "18.3.0-www-classic-5ee47d9f";
var ReactVersion = "18.3.0-www-classic-44782c01";

// ATTENTION
// When adding new symbols to this file,
Expand Down
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ if (
}
"use strict";

var ReactVersion = "18.3.0-www-modern-85b0d0c8";
var ReactVersion = "18.3.0-www-modern-c6e988f2";

// ATTENTION
// When adding new symbols to this file,
Expand Down
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -615,4 +615,4 @@ exports.useSyncExternalStore = function (
exports.useTransition = function () {
return ReactCurrentDispatcher.current.useTransition();
};
exports.version = "18.3.0-www-modern-e80fccd0";
exports.version = "18.3.0-www-modern-cbca474e";
213 changes: 122 additions & 91 deletions compiled/facebook-www/ReactART-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function _assertThisInitialized(self) {
return self;
}

var ReactVersion = "18.3.0-www-classic-a63d3ca6";
var ReactVersion = "18.3.0-www-classic-958fdb2b";

var LegacyRoot = 0;
var ConcurrentRoot = 1;
Expand Down Expand Up @@ -7734,97 +7734,102 @@ var currentEntangledListeners = null; // The number of pending async actions in
var currentEntangledPendingCount = 0; // The transition lane shared by all updates in the entangled scope.

var currentEntangledLane = NoLane;
function requestAsyncActionContext(actionReturnValue, finishedState) {
if (
actionReturnValue !== null &&
typeof actionReturnValue === "object" &&
typeof actionReturnValue.then === "function"
) {
// This is an async action.
//
// Return a thenable that resolves once the action scope (i.e. the async
// function passed to startTransition) has finished running.
var thenable = actionReturnValue;
var entangledListeners;

if (currentEntangledListeners === null) {
// There's no outer async action scope. Create a new one.
entangledListeners = currentEntangledListeners = [];
currentEntangledPendingCount = 0;
currentEntangledLane = requestTransitionLane();
} else {
entangledListeners = currentEntangledListeners;
function requestAsyncActionContext(
actionReturnValue, // If this is provided, this resulting thenable resolves to this value instead
// of the return value of the action. This is a perf trick to avoid composing
// an extra async function.
overrideReturnValue
) {
// This is an async action.
//
// Return a thenable that resolves once the action scope (i.e. the async
// function passed to startTransition) has finished running.
var thenable = actionReturnValue;
var entangledListeners;

if (currentEntangledListeners === null) {
// There's no outer async action scope. Create a new one.
entangledListeners = currentEntangledListeners = [];
currentEntangledPendingCount = 0;
currentEntangledLane = requestTransitionLane();
} else {
entangledListeners = currentEntangledListeners;
}

currentEntangledPendingCount++; // Create a thenable that represents the result of this action, but doesn't
// resolve until the entire entangled scope has finished.
//
// Expressed using promises:
// const [thisResult] = await Promise.all([thisAction, entangledAction]);
// return thisResult;

var resultThenable = createResultThenable(entangledListeners);
var resultStatus = "pending";
var resultValue;
var rejectedReason;
thenable.then(
function (value) {
resultStatus = "fulfilled";
resultValue = overrideReturnValue !== null ? overrideReturnValue : value;
pingEngtangledActionScope();
},
function (error) {
resultStatus = "rejected";
rejectedReason = error;
pingEngtangledActionScope();
}
); // Attach a listener to fill in the result.

currentEntangledPendingCount++;
var resultStatus = "pending";
var rejectedReason;
thenable.then(
function () {
resultStatus = "fulfilled";
pingEngtangledActionScope();
},
function (error) {
resultStatus = "rejected";
rejectedReason = error;
pingEngtangledActionScope();
entangledListeners.push(function () {
switch (resultStatus) {
case "fulfilled": {
var fulfilledThenable = resultThenable;
fulfilledThenable.status = "fulfilled";
fulfilledThenable.value = resultValue;
break;
}
); // Create a thenable that represents the result of this action, but doesn't
// resolve until the entire entangled scope has finished.
//
// Expressed using promises:
// const [thisResult] = await Promise.all([thisAction, entangledAction]);
// return thisResult;

var resultThenable = createResultThenable(entangledListeners); // Attach a listener to fill in the result.

entangledListeners.push(function () {
switch (resultStatus) {
case "fulfilled": {
var fulfilledThenable = resultThenable;
fulfilledThenable.status = "fulfilled";
fulfilledThenable.value = finishedState;
break;
}

case "rejected": {
var rejectedThenable = resultThenable;
rejectedThenable.status = "rejected";
rejectedThenable.reason = rejectedReason;
break;
}
case "rejected": {
var rejectedThenable = resultThenable;
rejectedThenable.status = "rejected";
rejectedThenable.reason = rejectedReason;
break;
}

case "pending":
default: {
// The listener above should have been called first, so `resultStatus`
// should already be set to the correct value.
throw new Error(
"Thenable should have already resolved. This " +
"is a bug in React."
);
}
case "pending":
default: {
// The listener above should have been called first, so `resultStatus`
// should already be set to the correct value.
throw new Error(
"Thenable should have already resolved. This " + "is a bug in React."
);
}
}
});
return resultThenable;
}
function requestSyncActionContext(
actionReturnValue, // If this is provided, this resulting thenable resolves to this value instead
// of the return value of the action. This is a perf trick to avoid composing
// an extra async function.
overrideReturnValue
) {
var resultValue =
overrideReturnValue !== null ? overrideReturnValue : actionReturnValue; // This is not an async action, but it may be part of an outer async action.

if (currentEntangledListeners === null) {
return resultValue;
} else {
// Return a thenable that does not resolve until the entangled actions
// have finished.
var entangledListeners = currentEntangledListeners;
var resultThenable = createResultThenable(entangledListeners);
entangledListeners.push(function () {
var fulfilledThenable = resultThenable;
fulfilledThenable.status = "fulfilled";
fulfilledThenable.value = resultValue;
});
return resultThenable;
} else {
// This is not an async action, but it may be part of an outer async action.
if (currentEntangledListeners === null) {
return finishedState;
} else {
// Return a thenable that does not resolve until the entangled actions
// have finished.
var _entangledListeners = currentEntangledListeners;

var _resultThenable = createResultThenable(_entangledListeners);

_entangledListeners.push(function () {
var fulfilledThenable = _resultThenable;
fulfilledThenable.status = "fulfilled";
fulfilledThenable.value = finishedState;
});

return _resultThenable;
}
}
}

Expand Down Expand Up @@ -9325,7 +9330,7 @@ function rerenderOptimistic(passthrough, reducer) {
hook.baseState = hook.memoizedState = passthrough;
var dispatch = hook.queue.dispatch;
return [passthrough, dispatch];
}
} // useFormState actions run sequentially, because each action receives the

function pushEffect(tag, create, inst, deps) {
var effect = {
Expand Down Expand Up @@ -9851,13 +9856,39 @@ function startTransition(

try {
if (enableAsyncActions) {
var returnValue = callback(); // This is either `finishedState` or a thenable that resolves to
// `finishedState`, depending on whether the action scope is an async
// function. In the async case, the resulting render will suspend until
// the async action scope has finished.
var returnValue = callback(); // Check if we're inside an async action scope. If so, we'll entangle
// this new action with the existing scope.
//
// If we're not already inside an async action scope, and this action is
// async, then we'll create a new async scope.
//
// In the async case, the resulting render will suspend until the async
// action scope has finished.

if (
returnValue !== null &&
typeof returnValue === "object" &&
typeof returnValue.then === "function"
) {
var thenable = returnValue; // This is a thenable that resolves to `finishedState` once the async
// action scope has finished.

var maybeThenable = requestAsyncActionContext(returnValue, finishedState);
dispatchSetState(fiber, queue, maybeThenable);
var entangledResult = requestAsyncActionContext(
thenable,
finishedState
);
dispatchSetState(fiber, queue, entangledResult);
} else {
// This is either `finishedState` or a thenable that resolves to
// `finishedState`, depending on whether we're inside an async
// action scope.
var _entangledResult = requestSyncActionContext(
returnValue,
finishedState
);

dispatchSetState(fiber, queue, _entangledResult);
}
} else {
// Async actions are not enabled.
dispatchSetState(fiber, queue, finishedState);
Expand Down
Loading

0 comments on commit eeb3579

Please sign in to comment.