Skip to content

Commit 17eaaca

Browse files
authored
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.
1 parent 3e6bc7d commit 17eaaca

File tree

9 files changed

+190
-93
lines changed

9 files changed

+190
-93
lines changed

packages/react-debug-tools/src/ReactDebugHooks.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,8 +521,9 @@ function useFormState<S, P>(
521521
action: (Awaited<S>, P) => S,
522522
initialState: Awaited<S>,
523523
permalink?: string,
524-
): [Awaited<S>, (P) => void] {
524+
): [Awaited<S>, (P) => void, boolean] {
525525
const hook = nextHook(); // FormState
526+
nextHook(); // PendingState
526527
nextHook(); // ActionQueue
527528
const stackError = new Error();
528529
let value;
@@ -580,7 +581,9 @@ function useFormState<S, P>(
580581
// value being a Thenable is equivalent to error being not null
581582
// i.e. we only reach this point with Awaited<S>
582583
const state = ((value: any): Awaited<S>);
583-
return [state, (payload: P) => {}];
584+
585+
// TODO: support displaying pending value
586+
return [state, (payload: P) => {}, false];
584587
}
585588

586589
const Dispatcher: DispatcherType = {

packages/react-dom-bindings/src/shared/ReactDOMFormActions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export function useFormState<S, P>(
8080
action: (Awaited<S>, P) => S,
8181
initialState: Awaited<S>,
8282
permalink?: string,
83-
): [Awaited<S>, (P) => void] {
83+
): [Awaited<S>, (P) => void, boolean] {
8484
if (!(enableFormActions && enableAsyncActions)) {
8585
throw new Error('Not implemented.');
8686
} else {

packages/react-dom/index.experimental.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function experimental_useFormState<S, P>(
4545
action: (Awaited<S>, P) => S,
4646
initialState: Awaited<S>,
4747
permalink?: string,
48-
): [Awaited<S>, (P) => void] {
48+
): [Awaited<S>, (P) => void, boolean] {
4949
if (__DEV__) {
5050
console.error(
5151
'useFormState is now in canary. Remove the experimental_ prefix. ' +

packages/react-dom/server-rendering-stub.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function experimental_useFormState<S, P>(
5050
action: (Awaited<S>, P) => S,
5151
initialState: Awaited<S>,
5252
permalink?: string,
53-
): [Awaited<S>, (P) => void] {
53+
): [Awaited<S>, (P) => void, boolean] {
5454
if (__DEV__) {
5555
console.error(
5656
'useFormState is now in canary. Remove the experimental_ prefix. ' +

packages/react-dom/src/__tests__/ReactDOMForm-test.js

Lines changed: 53 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,6 @@ describe('ReactDOMForm', () => {
6363
textCache = new Map();
6464
});
6565

66-
function resolveText(text) {
67-
const record = textCache.get(text);
68-
if (record === undefined) {
69-
const newRecord = {
70-
status: 'resolved',
71-
value: text,
72-
};
73-
textCache.set(text, newRecord);
74-
} else if (record.status === 'pending') {
75-
const thenable = record.value;
76-
record.status = 'resolved';
77-
record.value = text;
78-
thenable.pings.forEach(t => t());
79-
}
80-
}
8166
function resolveText(text) {
8267
const record = textCache.get(text);
8368
if (record === undefined) {
@@ -997,19 +982,20 @@ describe('ReactDOMForm', () => {
997982

998983
let dispatch;
999984
function App() {
1000-
const [state, _dispatch] = useFormState(action, 0);
985+
const [state, _dispatch, isPending] = useFormState(action, 0);
1001986
dispatch = _dispatch;
1002-
return <Text text={state} />;
987+
const pending = isPending ? 'Pending ' : '';
988+
return <Text text={pending + state} />;
1003989
}
1004990

1005991
const root = ReactDOMClient.createRoot(container);
1006992
await act(() => root.render(<App />));
1007-
assertLog([0]);
993+
assertLog(['0']);
1008994
expect(container.textContent).toBe('0');
1009995

1010996
await act(() => dispatch('increment'));
1011-
assertLog(['Async action started [1]']);
1012-
expect(container.textContent).toBe('0');
997+
assertLog(['Async action started [1]', 'Pending 0']);
998+
expect(container.textContent).toBe('Pending 0');
1013999

10141000
// Dispatch a few more actions. None of these will start until the previous
10151001
// one finishes.
@@ -1031,7 +1017,7 @@ describe('ReactDOMForm', () => {
10311017
await act(() => resolveText('Wait [4]'));
10321018

10331019
// Finally the last action finishes and we can render the result.
1034-
assertLog([2]);
1020+
assertLog(['2']);
10351021
expect(container.textContent).toBe('2');
10361022
});
10371023

@@ -1040,40 +1026,42 @@ describe('ReactDOMForm', () => {
10401026
test('useFormState supports inline actions', async () => {
10411027
let increment;
10421028
function App({stepSize}) {
1043-
const [state, dispatch] = useFormState(async prevState => {
1029+
const [state, dispatch, isPending] = useFormState(async prevState => {
10441030
return prevState + stepSize;
10451031
}, 0);
10461032
increment = dispatch;
1047-
return <Text text={state} />;
1033+
const pending = isPending ? 'Pending ' : '';
1034+
return <Text text={pending + state} />;
10481035
}
10491036

10501037
// Initial render
10511038
const root = ReactDOMClient.createRoot(container);
10521039
await act(() => root.render(<App stepSize={1} />));
1053-
assertLog([0]);
1040+
assertLog(['0']);
10541041

10551042
// Perform an action. This will increase the state by 1, as defined by the
10561043
// stepSize prop.
10571044
await act(() => increment());
1058-
assertLog([1]);
1045+
assertLog(['Pending 0', '1']);
10591046

10601047
// Now increase the stepSize prop to 10. Subsequent steps will increase
10611048
// by this amount.
10621049
await act(() => root.render(<App stepSize={10} />));
1063-
assertLog([1]);
1050+
assertLog(['1']);
10641051

10651052
// Increment again. The state should increase by 10.
10661053
await act(() => increment());
1067-
assertLog([11]);
1054+
assertLog(['Pending 1', '11']);
10681055
});
10691056

10701057
// @gate enableFormActions
10711058
// @gate enableAsyncActions
10721059
test('useFormState: dispatch throws if called during render', async () => {
10731060
function App() {
1074-
const [state, dispatch] = useFormState(async () => {}, 0);
1061+
const [state, dispatch, isPending] = useFormState(async () => {}, 0);
10751062
dispatch();
1076-
return <Text text={state} />;
1063+
const pending = isPending ? 'Pending ' : '';
1064+
return <Text text={pending + state} />;
10771065
}
10781066

10791067
const root = ReactDOMClient.createRoot(container);
@@ -1088,21 +1076,25 @@ describe('ReactDOMForm', () => {
10881076
test('queues multiple actions and runs them in order', async () => {
10891077
let action;
10901078
function App() {
1091-
const [state, dispatch] = useFormState(
1079+
const [state, dispatch, isPending] = useFormState(
10921080
async (s, a) => await getText(a),
10931081
'A',
10941082
);
10951083
action = dispatch;
1096-
return <Text text={state} />;
1084+
const pending = isPending ? 'Pending ' : '';
1085+
return <Text text={pending + state} />;
10971086
}
10981087

10991088
const root = ReactDOMClient.createRoot(container);
11001089
await act(() => root.render(<App />));
11011090
assertLog(['A']);
11021091

11031092
await act(() => action('B'));
1093+
// The first dispatch will update the pending state.
1094+
assertLog(['Pending A']);
11041095
await act(() => action('C'));
11051096
await act(() => action('D'));
1097+
assertLog([]);
11061098

11071099
await act(() => resolveText('B'));
11081100
await act(() => resolveText('C'));
@@ -1117,51 +1109,56 @@ describe('ReactDOMForm', () => {
11171109
test('useFormState: works if action is sync', async () => {
11181110
let increment;
11191111
function App({stepSize}) {
1120-
const [state, dispatch] = useFormState(prevState => {
1112+
const [state, dispatch, isPending] = useFormState(prevState => {
11211113
return prevState + stepSize;
11221114
}, 0);
11231115
increment = dispatch;
1124-
return <Text text={state} />;
1116+
const pending = isPending ? 'Pending ' : '';
1117+
return <Text text={pending + state} />;
11251118
}
11261119

11271120
// Initial render
11281121
const root = ReactDOMClient.createRoot(container);
11291122
await act(() => root.render(<App stepSize={1} />));
1130-
assertLog([0]);
1123+
assertLog(['0']);
11311124

11321125
// Perform an action. This will increase the state by 1, as defined by the
11331126
// stepSize prop.
11341127
await act(() => increment());
1135-
assertLog([1]);
1128+
assertLog(['Pending 0', '1']);
11361129

11371130
// Now increase the stepSize prop to 10. Subsequent steps will increase
11381131
// by this amount.
11391132
await act(() => root.render(<App stepSize={10} />));
1140-
assertLog([1]);
1133+
assertLog(['1']);
11411134

11421135
// Increment again. The state should increase by 10.
11431136
await act(() => increment());
1144-
assertLog([11]);
1137+
assertLog(['Pending 1', '11']);
11451138
});
11461139

11471140
// @gate enableFormActions
11481141
// @gate enableAsyncActions
11491142
test('useFormState: can mix sync and async actions', async () => {
11501143
let action;
11511144
function App() {
1152-
const [state, dispatch] = useFormState((s, a) => a, 'A');
1145+
const [state, dispatch, isPending] = useFormState((s, a) => a, 'A');
11531146
action = dispatch;
1154-
return <Text text={state} />;
1147+
const pending = isPending ? 'Pending ' : '';
1148+
return <Text text={pending + state} />;
11551149
}
11561150

11571151
const root = ReactDOMClient.createRoot(container);
11581152
await act(() => root.render(<App />));
11591153
assertLog(['A']);
11601154

11611155
await act(() => action(getText('B')));
1156+
// The first dispatch will update the pending state.
1157+
assertLog(['Pending A']);
11621158
await act(() => action('C'));
11631159
await act(() => action(getText('D')));
11641160
await act(() => action('E'));
1161+
assertLog([]);
11651162

11661163
await act(() => resolveText('B'));
11671164
await act(() => resolveText('D'));
@@ -1189,14 +1186,15 @@ describe('ReactDOMForm', () => {
11891186

11901187
let action;
11911188
function App() {
1192-
const [state, dispatch] = useFormState((s, a) => {
1189+
const [state, dispatch, isPending] = useFormState((s, a) => {
11931190
if (a.endsWith('!')) {
11941191
throw new Error(a);
11951192
}
11961193
return a;
11971194
}, 'A');
11981195
action = dispatch;
1199-
return <Text text={state} />;
1196+
const pending = isPending ? 'Pending ' : '';
1197+
return <Text text={pending + state} />;
12001198
}
12011199

12021200
const root = ReactDOMClient.createRoot(container);
@@ -1210,7 +1208,13 @@ describe('ReactDOMForm', () => {
12101208
assertLog(['A']);
12111209

12121210
await act(() => action('Oops!'));
1213-
assertLog(['Caught an error: Oops!', 'Caught an error: Oops!']);
1211+
assertLog([
1212+
// Action begins, error has not thrown yet.
1213+
'Pending A',
1214+
// Now the action runs and throws.
1215+
'Caught an error: Oops!',
1216+
'Caught an error: Oops!',
1217+
]);
12141218
expect(container.textContent).toBe('Caught an error: Oops!');
12151219

12161220
// Reset the error boundary
@@ -1223,7 +1227,7 @@ describe('ReactDOMForm', () => {
12231227
action('Oops!');
12241228
action('B');
12251229
});
1226-
assertLog(['B']);
1230+
assertLog(['Pending A', 'B']);
12271231
expect(container.textContent).toBe('B');
12281232
});
12291233

@@ -1247,15 +1251,16 @@ describe('ReactDOMForm', () => {
12471251

12481252
let action;
12491253
function App() {
1250-
const [state, dispatch] = useFormState(async (s, a) => {
1254+
const [state, dispatch, isPending] = useFormState(async (s, a) => {
12511255
const text = await getText(a);
12521256
if (text.endsWith('!')) {
12531257
throw new Error(text);
12541258
}
12551259
return text;
12561260
}, 'A');
12571261
action = dispatch;
1258-
return <Text text={state} />;
1262+
const pending = isPending ? 'Pending ' : '';
1263+
return <Text text={pending + state} />;
12591264
}
12601265

12611266
const root = ReactDOMClient.createRoot(container);
@@ -1269,7 +1274,8 @@ describe('ReactDOMForm', () => {
12691274
assertLog(['A']);
12701275

12711276
await act(() => action('Oops!'));
1272-
assertLog([]);
1277+
// The first dispatch will update the pending state.
1278+
assertLog(['Pending A']);
12731279
await act(() => resolveText('Oops!'));
12741280
assertLog(['Caught an error: Oops!', 'Caught an error: Oops!']);
12751281
expect(container.textContent).toBe('Caught an error: Oops!');
@@ -1284,7 +1290,7 @@ describe('ReactDOMForm', () => {
12841290
action('Oops!');
12851291
action('B');
12861292
});
1287-
assertLog([]);
1293+
assertLog(['Pending A']);
12881294
await act(() => resolveText('B'));
12891295
assertLog(['B']);
12901296
expect(container.textContent).toBe('B');

0 commit comments

Comments
 (0)