[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.
Andrew Clark committed
Oct 31, 2023 at 23:32 UTC
77c4ac2ce88736bbdfe0b29008b5df931c2beb1e
31 files changed
+667
-471
fixtures/flight/config/modules.js
+5
-3
@@ -108,9 +108,11 @@ function getModules() {
108
// TypeScript project and set up the config
109
// based on tsconfig.json
110
if (hasTsConfig) {
111
- const ts = require(resolve.sync('typescript', {
112
- basedir: paths.appNodeModules,
113
- }));
111
+ const ts = require(
112
+ resolve.sync('typescript', {
113
+ basedir: paths.appNodeModules,
114
+ })
115
+ );
116
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
117
// Otherwise we'll check if there is jsconfig.json
118
// for non TS projects.
package.json
+2
-2
@@ -81,14 +81,14 @@
81
"minimist": "^1.2.3",
82
"mkdirp": "^0.5.1",
83
"ncp": "^2.0.0",
84
- "prettier": "2.8.3",
84
+ "prettier": "3.0.3",
85
"pretty-format": "^29.4.1",
86
"prop-types": "^15.6.2",
87
"random-seed": "^0.3.0",
88
"react-lifecycles-compat": "^3.0.4",
89
"rimraf": "^3.0.0",
90
"rollup": "^3.17.1",
91
- "rollup-plugin-prettier": "^3.0.0",
91
+ "rollup-plugin-prettier": "^4.1.1",
92
"rollup-plugin-strip-banner": "^3.0.0",
93
"semver": "^7.1.1",
94
"signedsource": "^2.0.0",
packages/react-client/src/ReactFlightClient.js
+5
-5
@@ -572,11 +572,11 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
572
}
573
// Since this is a fake Promise whose .then doesn't chain, we have to wrap it.
574
// TODO: Remove the wrapper once that's fixed.
575
- return ((Promise.resolve(p): any): Promise<Array<any>>).then(function (
576
- bound,
577
- ) {
578
- return callServer(metaData.id, bound.concat(args));
579
- });
575
+ return ((Promise.resolve(p): any): Promise<Array<any>>).then(
576
+ function (bound) {
577
+ return callServer(metaData.id, bound.concat(args));
578
+ },
579
+ );
580
};
581
registerServerReference(proxy, metaData);
582
return proxy;
packages/react-devtools-shared/src/__tests__/profilerChangeDescriptions-test.js
+5
-6
@@ -40,12 +40,11 @@ describe('Profiler change descriptions', () => {
40
}
41
42
const MemoizedChild = React.memo(Child, areEqual);
43
- const ForwardRefChild = React.forwardRef(function RefForwardingComponent(
44
- props,
45
- ref,
46
- ) {
47
- return <Child />;
48
- });
43
+ const ForwardRefChild = React.forwardRef(
44
+ function RefForwardingComponent(props, ref) {
45
+ return <Child />;
46
+ },
47
+ );
48
49
let forceUpdate = null;
50
packages/react-devtools-shared/src/devtools/ProfilingCache.js
+8
-8
@@ -33,20 +33,20 @@ export default class ProfilingCache {
33
this._profilerStore = profilerStore;
34
}
35
36
- getCommitTree: ({
37
- commitIndex: number,
38
- rootID: number,
39
- }) => CommitTree = ({commitIndex, rootID}) =>
36
+ getCommitTree: ({commitIndex: number, rootID: number}) => CommitTree = ({
37
+ commitIndex,
38
+ rootID,
39
+ }) =>
40
getCommitTree({
41
commitIndex,
42
profilerStore: this._profilerStore,
43
rootID,
44
});
45
46
- getFiberCommits: ({
47
- fiberID: number,
48
- rootID: number,
49
- }) => Array<number> = ({fiberID, rootID}) => {
46
+ getFiberCommits: ({fiberID: number, rootID: number}) => Array<number> = ({
47
+ fiberID,
48
+ rootID,
49
+ }) => {
50
const cachedFiberCommits = this._fiberCommits.get(fiberID);
51
if (cachedFiberCommits != null) {
52
return cachedFiberCommits;
packages/react-devtools-shared/src/hooks/__tests__/parseHookNames-test.js
+2
-3
@@ -70,9 +70,8 @@ describe('parseHookNames', () => {
70
const hooksList = flattenHooksList(hooksTree);
71
72
// Runs in the UI thread so it can share Network cache:
73
- const locationKeyToHookSourceAndMetadata = await loadSourceAndMetadata(
74
- hooksList,
75
- );
73
+ const locationKeyToHookSourceAndMetadata =
74
+ await loadSourceAndMetadata(hooksList);
75
76
// Runs in a Worker because it's CPU intensive:
77
return parseSourceAndMetadata(
packages/react-dom-bindings/src/server/ReactDOMServerExternalRuntime.js
+8
-8
@@ -26,7 +26,7 @@ if (document.body != null) {
26
installFizzInstrObserver(document.body);
27
}
28
// $FlowFixMe[incompatible-cast]
29
- handleExistingNodes((document.body /*: HTMLElement */));
29
+ handleExistingNodes((document.body: HTMLElement));
30
} else {
31
// Document must be loading -- body may not exist yet if the fizz external
32
// runtime is sent in <head> (e.g. as a preinit resource)
@@ -38,7 +38,7 @@ if (document.body != null) {
38
installFizzInstrObserver(document.body);
39
}
40
// $FlowFixMe[incompatible-cast]
41
- handleExistingNodes((document.body /*: HTMLElement */));
41
+ handleExistingNodes((document.body: HTMLElement));
42
43
// We can call disconnect without takeRecord here,
44
// since we only expect a single document.body
@@ -49,15 +49,15 @@ if (document.body != null) {
49
domBodyObserver.observe(document.documentElement, {childList: true});
50
}
51
52
-function handleExistingNodes(target /*: HTMLElement */) {
52
+function handleExistingNodes(target: HTMLElement) {
53
const existingNodes = target.querySelectorAll('template');
54
for (let i = 0; i < existingNodes.length; i++) {
55
handleNode(existingNodes[i]);
56
}
57
}
58
59
-function installFizzInstrObserver(target /*: Node */) {
60
- const handleMutations = (mutations /*: Array<MutationRecord> */) => {
59
+function installFizzInstrObserver(target: Node) {
60
+ const handleMutations = (mutations: Array<MutationRecord>) => {
61
for (let i = 0; i < mutations.length; i++) {
62
const addedNodes = mutations[i].addedNodes;
63
for (let j = 0; j < addedNodes.length; j++) {
@@ -80,13 +80,13 @@ function installFizzInstrObserver(target /*: Node */) {
80
});
81
}
82
83
-function handleNode(node_ /*: Node */) {
83
+function handleNode(node_: Node) {
84
// $FlowFixMe[incompatible-cast]
85
- if (node_.nodeType !== 1 || !(node_ /*: HTMLElement */).dataset) {
85
+ if (node_.nodeType !== 1 || !(node_: HTMLElement).dataset) {
86
return;
87
}
88
// $FlowFixMe[incompatible-cast]
89
- const node = (node_ /*: HTMLElement */);
89
+ const node = (node_: HTMLElement);
90
const dataset = node.dataset;
91
if (dataset['rxi'] != null) {
92
clientRenderBoundary(
packages/react-dom-bindings/src/shared/ReactDOMFormActions.js
+4
-3
@@ -8,6 +8,7 @@
8
*/
9
10
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
11
+import type {Awaited} from 'shared/ReactTypes';
12
13
import {enableAsyncActions, enableFormActions} from 'shared/ReactFeatureFlags';
14
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -76,10 +77,10 @@ export function useFormStatus(): FormStatus {
77
}
78
79
export function useFormState<S, P>(
79
- action: (S, P) => Promise<S>,
80
- initialState: S,
80
+ action: (Awaited<S>, P) => S,
81
+ initialState: Awaited<S>,
82
permalink?: string,
82
-): [S, (P) => void] {
83
+): [Awaited<S>, (P) => void] {
84
if (!(enableFormActions && enableAsyncActions)) {
85
throw new Error('Not implemented.');
86
} else {
packages/react-dom/index.experimental.js
+4
-3
@@ -31,6 +31,7 @@ export {
31
version,
32
} from './src/client/ReactDOM';
33
34
+import type {Awaited} from 'shared/ReactTypes';
35
import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
36
import {useFormStatus, useFormState} from './src/client/ReactDOM';
37
@@ -45,10 +46,10 @@ export function experimental_useFormStatus(): FormStatus {
46
}
47
48
export function experimental_useFormState<S, P>(
48
- action: (S, P) => Promise<S>,
49
- initialState: S,
49
+ action: (Awaited<S>, P) => S,
50
+ initialState: Awaited<S>,
51
permalink?: string,
51
-): [S, (P) => void] {
52
+): [Awaited<S>, (P) => void] {
53
if (__DEV__) {
54
console.error(
55
'useFormState is now in canary. Remove the experimental_ prefix. ' +
packages/react-dom/server-rendering-stub.js
+4
-3
@@ -34,6 +34,7 @@ import {
34
useFormStatus,
35
useFormState,
36
} from './src/server/ReactDOMServerRenderingStub';
37
+import type {Awaited} from 'shared/ReactTypes';
38
39
export function experimental_useFormStatus(): FormStatus {
40
if (__DEV__) {
@@ -46,10 +47,10 @@ export function experimental_useFormStatus(): FormStatus {
47
}
48
49
export function experimental_useFormState<S, P>(
49
- action: (S, P) => Promise<S>,
50
- initialState: S,
50
+ action: (Awaited<S>, P) => S,
51
+ initialState: Awaited<S>,
52
permalink?: string,
52
-): [S, (P) => void] {
53
+): [Awaited<S>, (P) => void] {
54
if (__DEV__) {
55
console.error(
56
'useFormState is now in canary. Remove the experimental_ prefix. ' +
packages/react-dom/src/__tests__/ReactDOMForm-test.js
+166
-16
@@ -1113,29 +1113,179 @@ describe('ReactDOMForm', () => {
1113
1114
// @gate enableFormActions
1115
// @gate enableAsyncActions
1116
- test('useFormState: warns if action is not async', async () => {
1117
- let dispatch;
1116
+ test('useFormState: works if action is sync', async () => {
1117
+ let increment;
1118
+ function App({stepSize}) {
1119
+ const [state, dispatch] = useFormState(prevState => {
1120
+ return prevState + stepSize;
1121
+ }, 0);
1122
+ increment = dispatch;
1123
+ return <Text text={state} />;
1124
+ }
1125
+
1126
+ // Initial render
1127
+ const root = ReactDOMClient.createRoot(container);
1128
+ await act(() => root.render(<App stepSize={1} />));
1129
+ assertLog([0]);
1130
+
1131
+ // Perform an action. This will increase the state by 1, as defined by the
1132
+ // stepSize prop.
1133
+ await act(() => increment());
1134
+ assertLog([1]);
1135
+
1136
+ // Now increase the stepSize prop to 10. Subsequent steps will increase
1137
+ // by this amount.
1138
+ await act(() => root.render(<App stepSize={10} />));
1139
+ assertLog([1]);
1140
+
1141
+ // Increment again. The state should increase by 10.
1142
+ await act(() => increment());
1143
+ assertLog([11]);
1144
+ });
1145
+
1146
+ // @gate enableFormActions
1147
+ // @gate enableAsyncActions
1148
+ test('useFormState: can mix sync and async actions', async () => {
1149
+ let action;
1150
function App() {
1119
- const [state, _dispatch] = useFormState(() => {}, 0);
1120
- dispatch = _dispatch;
1151
+ const [state, dispatch] = useFormState((s, a) => a, 'A');
1152
+ action = dispatch;
1153
return <Text text={state} />;
1154
}
1155
1156
const root = ReactDOMClient.createRoot(container);
1125
- await act(async () => {
1126
- root.render(<App />);
1157
+ await act(() => root.render(<App />));
1158
+ assertLog(['A']);
1159
+
1160
+ await act(() => action(getText('B')));
1161
+ await act(() => action('C'));
1162
+ await act(() => action(getText('D')));
1163
+ await act(() => action('E'));
1164
+
1165
+ await act(() => resolveText('B'));
1166
+ await act(() => resolveText('D'));
1167
+ assertLog(['E']);
1168
+ expect(container.textContent).toBe('E');
1169
+ });
1170
+
1171
+ // @gate enableFormActions
1172
+ // @gate enableAsyncActions
1173
+ test('useFormState: error handling (sync action)', async () => {
1174
+ let resetErrorBoundary;
1175
+ class ErrorBoundary extends React.Component {
1176
+ state = {error: null};
1177
+ static getDerivedStateFromError(error) {
1178
+ return {error};
1179
+ }
1180
+ render() {
1181
+ resetErrorBoundary = () => this.setState({error: null});
1182
+ if (this.state.error !== null) {
1183
+ return <Text text={'Caught an error: ' + this.state.error.message} />;
1184
+ }
1185
+ return this.props.children;
1186
+ }
1187
+ }
1188
+
1189
+ let action;
1190
+ function App() {
1191
+ const [state, dispatch] = useFormState((s, a) => {
1192
+ if (a.endsWith('!')) {
1193
+ throw new Error(a);
1194
+ }
1195
+ return a;
1196
+ }, 'A');
1197
+ action = dispatch;
1198
+ return <Text text={state} />;
1199
+ }
1200
+
1201
+ const root = ReactDOMClient.createRoot(container);
1202
+ await act(() =>
1203
+ root.render(
1204
+ <ErrorBoundary>
1205
+ <App />
1206
+ </ErrorBoundary>,
1207
+ ),
1208
+ );
1209
+ assertLog(['A']);
1210
+
1211
+ await act(() => action('Oops!'));
1212
+ assertLog(['Caught an error: Oops!', 'Caught an error: Oops!']);
1213
+ expect(container.textContent).toBe('Caught an error: Oops!');
1214
+
1215
+ // Reset the error boundary
1216
+ await act(() => resetErrorBoundary());
1217
+ assertLog(['A']);
1218
+
1219
+ // Trigger an error again, but this time, perform another action that
1220
+ // overrides the first one and fixes the error
1221
+ await act(() => {
1222
+ action('Oops!');
1223
+ action('B');
1224
});
1128
- assertLog([0]);
1225
+ assertLog(['B']);
1226
+ expect(container.textContent).toBe('B');
1227
+ });
1228
+
1229
+ // @gate enableFormActions
1230
+ // @gate enableAsyncActions
1231
+ test('useFormState: error handling (async action)', async () => {
1232
+ let resetErrorBoundary;
1233
+ class ErrorBoundary extends React.Component {
1234
+ state = {error: null};
1235
+ static getDerivedStateFromError(error) {
1236
+ return {error};
1237
+ }
1238
+ render() {
1239
+ resetErrorBoundary = () => this.setState({error: null});
1240
+ if (this.state.error !== null) {
1241
+ return <Text text={'Caught an error: ' + this.state.error.message} />;
1242
+ }
1243
+ return this.props.children;
1244
+ }
1245
+ }
1246
+
1247
+ let action;
1248
+ function App() {
1249
+ const [state, dispatch] = useFormState(async (s, a) => {
1250
+ const text = await getText(a);
1251
+ if (text.endsWith('!')) {
1252
+ throw new Error(text);
1253
+ }
1254
+ return text;
1255
+ }, 'A');
1256
+ action = dispatch;
1257
+ return <Text text={state} />;
1258
+ }
1259
1130
- expect(() => {
1131
- // This throws because React expects the action to return a promise.
1132
- expect(() => dispatch()).toThrow('Cannot read properties of undefined');
1133
- }).toErrorDev(
1134
- [
1135
- // In dev we also log a warning.
1136
- 'The action passed to useFormState must be an async function',
1137
- ],
1138
- {withoutStack: true},
1260
+ const root = ReactDOMClient.createRoot(container);
1261
+ await act(() =>
1262
+ root.render(
1263
+ <ErrorBoundary>
1264
+ <App />
1265
+ </ErrorBoundary>,
1266
+ ),
1267
);
1268
+ assertLog(['A']);
1269
+
1270
+ await act(() => action('Oops!'));
1271
+ assertLog([]);
1272
+ await act(() => resolveText('Oops!'));
1273
+ assertLog(['Caught an error: Oops!', 'Caught an error: Oops!']);
1274
+ expect(container.textContent).toBe('Caught an error: Oops!');
1275
+
1276
+ // Reset the error boundary
1277
+ await act(() => resetErrorBoundary());
1278
+ assertLog(['A']);
1279
+
1280
+ // Trigger an error again, but this time, perform another action that
1281
+ // overrides the first one and fixes the error
1282
+ await act(() => {
1283
+ action('Oops!');
1284
+ action('B');
1285
+ });
1286
+ assertLog([]);
1287
+ await act(() => resolveText('B'));
1288
+ assertLog(['B']);
1289
+ expect(container.textContent).toBe('B');
1290
});
1291
});
packages/react-dom/src/__tests__/ReactDOMInput-test.js
+98
-97
@@ -2089,38 +2089,40 @@ describe('ReactDOMInput', () => {
2089
it('sets type, step, min, max before value always', () => {
2090
const log = [];
2091
const originalCreateElement = document.createElement;
2092
- spyOnDevAndProd(document, 'createElement').mockImplementation(function (
2093
- type,
2094
- ) {
2095
- const el = originalCreateElement.apply(this, arguments);
2096
- let value = '';
2097
- let typeProp = '';
2098
-
2099
- if (type === 'input') {
2100
- Object.defineProperty(el, 'type', {
2101
- get: function () {
2102
- return typeProp;
2103
- },
2104
- set: function (val) {
2105
- typeProp = String(val);
2106
- log.push('set property type');
2107
- },
2108
- });
2109
- Object.defineProperty(el, 'value', {
2110
- get: function () {
2111
- return value;
2112
- },
2113
- set: function (val) {
2114
- value = String(val);
2115
- log.push('set property value');
2116
- },
2117
- });
2118
- spyOnDevAndProd(el, 'setAttribute').mockImplementation(function (name) {
2119
- log.push('set attribute ' + name);
2120
- });
2121
- }
2122
- return el;
2123
- });
2092
+ spyOnDevAndProd(document, 'createElement').mockImplementation(
2093
+ function (type) {
2094
+ const el = originalCreateElement.apply(this, arguments);
2095
+ let value = '';
2096
+ let typeProp = '';
2097
+
2098
+ if (type === 'input') {
2099
+ Object.defineProperty(el, 'type', {
2100
+ get: function () {
2101
+ return typeProp;
2102
+ },
2103
+ set: function (val) {
2104
+ typeProp = String(val);
2105
+ log.push('set property type');
2106
+ },
2107
+ });
2108
+ Object.defineProperty(el, 'value', {
2109
+ get: function () {
2110
+ return value;
2111
+ },
2112
+ set: function (val) {
2113
+ value = String(val);
2114
+ log.push('set property value');
2115
+ },
2116
+ });
2117
+ spyOnDevAndProd(el, 'setAttribute').mockImplementation(
2118
+ function (name) {
2119
+ log.push('set attribute ' + name);
2120
+ },
2121
+ );
2122
+ }
2123
+ return el;
2124
+ },
2125
+ );
2126
2127
ReactDOM.render(
2128
<input
@@ -2174,71 +2176,70 @@ describe('ReactDOMInput', () => {
2176
2177
const log = [];
2178
const originalCreateElement = document.createElement;
2177
- spyOnDevAndProd(document, 'createElement').mockImplementation(function (
2178
- type,
2179
- ) {
2180
- const el = originalCreateElement.apply(this, arguments);
2181
- const getDefaultValue = Object.getOwnPropertyDescriptor(
2182
- HTMLInputElement.prototype,
2183
- 'defaultValue',
2184
- ).get;
2185
- const setDefaultValue = Object.getOwnPropertyDescriptor(
2186
- HTMLInputElement.prototype,
2187
- 'defaultValue',
2188
- ).set;
2189
- const getValue = Object.getOwnPropertyDescriptor(
2190
- HTMLInputElement.prototype,
2191
- 'value',
2192
- ).get;
2193
- const setValue = Object.getOwnPropertyDescriptor(
2194
- HTMLInputElement.prototype,
2195
- 'value',
2196
- ).set;
2197
- const getType = Object.getOwnPropertyDescriptor(
2198
- HTMLInputElement.prototype,
2199
- 'type',
2200
- ).get;
2201
- const setType = Object.getOwnPropertyDescriptor(
2202
- HTMLInputElement.prototype,
2203
- 'type',
2204
- ).set;
2205
- if (type === 'input') {
2206
- Object.defineProperty(el, 'defaultValue', {
2207
- get: function () {
2208
- return getDefaultValue.call(this);
2209
- },
2210
- set: function (val) {
2211
- log.push(`node.defaultValue = ${strify(val)}`);
2212
- setDefaultValue.call(this, val);
2213
- },
2214
- });
2215
- Object.defineProperty(el, 'value', {
2216
- get: function () {
2217
- return getValue.call(this);
2218
- },
2219
- set: function (val) {
2220
- log.push(`node.value = ${strify(val)}`);
2221
- setValue.call(this, val);
2222
- },
2223
- });
2224
- Object.defineProperty(el, 'type', {
2225
- get: function () {
2226
- return getType.call(this);
2227
- },
2228
- set: function (val) {
2229
- log.push(`node.type = ${strify(val)}`);
2230
- setType.call(this, val);
2231
- },
2232
- });
2233
- spyOnDevAndProd(el, 'setAttribute').mockImplementation(function (
2234
- name,
2235
- val,
2236
- ) {
2237
- log.push(`node.setAttribute(${strify(name)}, ${strify(val)})`);
2238
- });
2239
- }
2240
- return el;
2241
- });
2179
+ spyOnDevAndProd(document, 'createElement').mockImplementation(
2180
+ function (type) {
2181
+ const el = originalCreateElement.apply(this, arguments);
2182
+ const getDefaultValue = Object.getOwnPropertyDescriptor(
2183
+ HTMLInputElement.prototype,
2184
+ 'defaultValue',
2185
+ ).get;
2186
+ const setDefaultValue = Object.getOwnPropertyDescriptor(
2187
+ HTMLInputElement.prototype,
2188
+ 'defaultValue',
2189
+ ).set;
2190
+ const getValue = Object.getOwnPropertyDescriptor(
2191
+ HTMLInputElement.prototype,
2192
+ 'value',
2193
+ ).get;
2194
+ const setValue = Object.getOwnPropertyDescriptor(
2195
+ HTMLInputElement.prototype,
2196
+ 'value',
2197
+ ).set;
2198
+ const getType = Object.getOwnPropertyDescriptor(
2199
+ HTMLInputElement.prototype,
2200
+ 'type',
2201
+ ).get;
2202
+ const setType = Object.getOwnPropertyDescriptor(
2203
+ HTMLInputElement.prototype,
2204
+ 'type',
2205
+ ).set;
2206
+ if (type === 'input') {
2207
+ Object.defineProperty(el, 'defaultValue', {
2208
+ get: function () {
2209
+ return getDefaultValue.call(this);
2210
+ },
2211
+ set: function (val) {
2212
+ log.push(`node.defaultValue = ${strify(val)}`);
2213
+ setDefaultValue.call(this, val);
2214
+ },
2215
+ });
2216
+ Object.defineProperty(el, 'value', {
2217
+ get: function () {
2218
+ return getValue.call(this);
2219
+ },
2220
+ set: function (val) {
2221
+ log.push(`node.value = ${strify(val)}`);
2222
+ setValue.call(this, val);
2223
+ },
2224
+ });
2225
+ Object.defineProperty(el, 'type', {
2226
+ get: function () {
2227
+ return getType.call(this);
2228
+ },
2229
+ set: function (val) {
2230
+ log.push(`node.type = ${strify(val)}`);
2231
+ setType.call(this, val);
2232
+ },
2233
+ });
2234
+ spyOnDevAndProd(el, 'setAttribute').mockImplementation(
2235
+ function (name, val) {
2236
+ log.push(`node.setAttribute(${strify(name)}, ${strify(val)})`);
2237
+ },
2238
+ );
2239
+ }
2240
+ return el;
2241
+ },
2242
+ );
2243
2244
ReactDOM.render(<input type="date" defaultValue="1980-01-01" />, container);
2245
packages/react-dom/src/__tests__/ReactDOMTextarea-test.js
+18
-18
@@ -137,24 +137,24 @@ describe('ReactDOMTextarea', () => {
137
138
let counter = 0;
139
const originalCreateElement = document.createElement;
140
- spyOnDevAndProd(document, 'createElement').mockImplementation(function (
141
- type,
142
- ) {
143
- const el = originalCreateElement.apply(this, arguments);
144
- let value = '';
145
- if (type === 'textarea') {
146
- Object.defineProperty(el, 'value', {
147
- get: function () {
148
- return value;
149
- },
150
- set: function (val) {
151
- value = String(val);
152
- counter++;
153
- },
154
- });
155
- }
156
- return el;
157
- });
140
+ spyOnDevAndProd(document, 'createElement').mockImplementation(
141
+ function (type) {
142
+ const el = originalCreateElement.apply(this, arguments);
143
+ let value = '';
144
+ if (type === 'textarea') {
145
+ Object.defineProperty(el, 'value', {
146
+ get: function () {
147
+ return value;
148
+ },
149
+ set: function (val) {
150
+ value = String(val);
151
+ counter++;
152
+ },
153
+ });
154
+ }
155
+ return el;
156
+ },
157
+ );
158
159
ReactDOM.render(<textarea value="" readOnly={true} />, container);
160
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js
+55
-64
@@ -43,25 +43,21 @@ const RCTFabricUIManager = {
43
}
44
return result.join('\n');
45
},
46
- createNode: jest.fn(function createNode(
47
- reactTag,
48
- viewName,
49
- rootTag,
50
- props,
51
- eventTarget,
52
- ) {
53
- if (allocatedTags.has(reactTag)) {
54
- throw new Error(`Created two native views with tag ${reactTag}`);
55
- }
46
+ createNode: jest.fn(
47
+ function createNode(reactTag, viewName, rootTag, props, eventTarget) {
48
+ if (allocatedTags.has(reactTag)) {
49
+ throw new Error(`Created two native views with tag ${reactTag}`);
50
+ }
51
57
- allocatedTags.add(reactTag);
58
- return {
59
- reactTag: reactTag,
60
- viewName: viewName,
61
- props: props,
62
- children: [],
63
- };
64
- }),
52
+ allocatedTags.add(reactTag);
53
+ return {
54
+ reactTag: reactTag,
55
+ viewName: viewName,
56
+ props: props,
57
+ children: [],
58
+ };
59
+ },
60
+ ),
61
cloneNode: jest.fn(function cloneNode(node) {
62
return {
63
reactTag: node.reactTag,
@@ -70,28 +66,26 @@ const RCTFabricUIManager = {
66
children: node.children,
67
};
68
}),
73
- cloneNodeWithNewChildren: jest.fn(function cloneNodeWithNewChildren(
74
- node,
75
- children,
76
- ) {
77
- return {
78
- reactTag: node.reactTag,
79
- viewName: node.viewName,
80
- props: node.props,
81
- children: children ?? [],
82
- };
83
- }),
84
- cloneNodeWithNewProps: jest.fn(function cloneNodeWithNewProps(
85
- node,
86
- newPropsDiff,
87
- ) {
88
- return {
89
- reactTag: node.reactTag,
90
- viewName: node.viewName,
91
- props: {...node.props, ...newPropsDiff},
92
- children: node.children,
93
- };
94
- }),
69
+ cloneNodeWithNewChildren: jest.fn(
70
+ function cloneNodeWithNewChildren(node, children) {
71
+ return {
72
+ reactTag: node.reactTag,
73
+ viewName: node.viewName,
74
+ props: node.props,
75
+ children: children ?? [],
76
+ };
77
+ },
78
+ ),
79
+ cloneNodeWithNewProps: jest.fn(
80
+ function cloneNodeWithNewProps(node, newPropsDiff) {
81
+ return {
82
+ reactTag: node.reactTag,
83
+ viewName: node.viewName,
84
+ props: {...node.props, ...newPropsDiff},
85
+ children: node.children,
86
+ };
87
+ },
88
+ ),
89
cloneNodeWithNewChildrenAndProps: jest.fn(
90
function cloneNodeWithNewChildrenAndProps(node, newPropsDiff) {
91
let children = [];
@@ -171,34 +165,31 @@ const RCTFabricUIManager = {
165
166
return [10, 10, 100, 100];
167
}),
174
- measureLayout: jest.fn(function measureLayout(
175
- node,
176
- relativeNode,
177
- fail,
178
- success,
179
- ) {
180
- if (typeof node !== 'object') {
181
- throw new Error(
182
- `Expected node to be an object, was passed "${typeof node}"`,
183
- );
184
- }
168
+ measureLayout: jest.fn(
169
+ function measureLayout(node, relativeNode, fail, success) {
170
+ if (typeof node !== 'object') {
171
+ throw new Error(
172
+ `Expected node to be an object, was passed "${typeof node}"`,
173
+ );
174
+ }
175
186
- if (typeof node.viewName !== 'string') {
187
- throw new Error('Expected node to be a host node.');
188
- }
176
+ if (typeof node.viewName !== 'string') {
177
+ throw new Error('Expected node to be a host node.');
178
+ }
179
190
- if (typeof relativeNode !== 'object') {
191
- throw new Error(
192
- `Expected relative node to be an object, was passed "${typeof relativeNode}"`,
193
- );
194
- }
180
+ if (typeof relativeNode !== 'object') {
181
+ throw new Error(
182
+ `Expected relative node to be an object, was passed "${typeof relativeNode}"`,
183
+ );
184
+ }
185
196
- if (typeof relativeNode.viewName !== 'string') {
197
- throw new Error('Expected relative node to be a host node.');
198
- }
186
+ if (typeof relativeNode.viewName !== 'string') {
187
+ throw new Error('Expected relative node to be a host node.');
188
+ }
189
200
- success(1, 1, 100, 100);
201
- }),
190
+ success(1, 1, 100, 100);
191
+ },
192
+ ),
193
setIsJSResponder: jest.fn(),
194
};
195
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js
+13
-16
@@ -173,24 +173,21 @@ const RCTUIManager = {
173
174
callback(10, 10, 100, 100);
175
}),
176
- measureLayout: jest.fn(function measureLayout(
177
- tag,
178
- relativeTag,
179
- fail,
180
- success,
181
- ) {
182
- if (typeof tag !== 'number') {
183
- throw new Error(`Expected tag to be a number, was passed ${tag}`);
184
- }
176
+ measureLayout: jest.fn(
177
+ function measureLayout(tag, relativeTag, fail, success) {
178
+ if (typeof tag !== 'number') {
179
+ throw new Error(`Expected tag to be a number, was passed ${tag}`);
180
+ }
181
186
- if (typeof relativeTag !== 'number') {
187
- throw new Error(
188
- `Expected relativeTag to be a number, was passed ${relativeTag}`,
189
- );
190
- }
182
+ if (typeof relativeTag !== 'number') {
183
+ throw new Error(
184
+ `Expected relativeTag to be a number, was passed ${relativeTag}`,
185
+ );
186
+ }
187
192
- success(1, 1, 100, 100);
193
- }),
188
+ success(1, 1, 100, 100);
189
+ },
190
+ ),
191
__takeSnapshot: jest.fn(),
192
};
193
packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
+7
-8
@@ -528,14 +528,13 @@ describe('ReactFabric', () => {
528
}));
529
530
const snapshots = [];
531
- nativeFabricUIManager.completeRoot.mockImplementation(function (
532
- rootTag,
533
- newChildSet,
534
- ) {
535
- snapshots.push(
536
- nativeFabricUIManager.__dumpChildSetForJestTestsOnly(newChildSet),
537
- );
538
- });
531
+ nativeFabricUIManager.completeRoot.mockImplementation(
532
+ function (rootTag, newChildSet) {
533
+ snapshots.push(
534
+ nativeFabricUIManager.__dumpChildSetForJestTestsOnly(newChildSet),
535
+ );
536
+ },
537
+ );
538
539
await act(() => {
540
ReactFabric.render(
packages/react-reconciler/src/ReactFiberAsyncAction.js
+2
-2
@@ -34,7 +34,7 @@ let currentEntangledPendingCount: number = 0;
34
let currentEntangledLane: Lane = NoLane;
35
36
export function requestAsyncActionContext<S>(
37
- actionReturnValue: Thenable<mixed>,
37
+ actionReturnValue: Thenable<any>,
38
// If this is provided, this resulting thenable resolves to this value instead
39
// of the return value of the action. This is a perf trick to avoid composing
40
// an extra async function.
@@ -112,7 +112,7 @@ export function requestAsyncActionContext<S>(
112
}
113
114
export function requestSyncActionContext<S>(
115
- actionReturnValue: mixed,
115
+ actionReturnValue: any,
116
// If this is provided, this resulting thenable resolves to this value instead
117
// of the return value of the action. This is a perf trick to avoid composing
118
// an extra async function.
packages/react-reconciler/src/ReactFiberHooks.js
+102
-89
@@ -13,6 +13,7 @@ import type {
13
Usable,
14
Thenable,
15
RejectedThenable,
16
+ Awaited,
17
} from 'shared/ReactTypes';
18
import type {
19
Fiber,
@@ -1871,12 +1872,12 @@ function rerenderOptimistic<S, A>(
1872
type FormStateActionQueue<S, P> = {
1873
// This is the most recent state returned from an action. It's updated as
1874
// soon as the action finishes running.
1874
- state: S,
1875
+ state: Awaited<S>,
1876
// A stable dispatch method, passed to the user.
1877
dispatch: Dispatch<P>,
1878
// This is the most recent action function that was rendered. It's updated
1879
// during the commit phase.
1879
- action: (S, P) => Promise<S>,
1880
+ action: (Awaited<S>, P) => S,
1881
// This is a circular linked list of pending action payloads. It incudes the
1882
// action that is currently running.
1883
pending: FormStateActionQueueNode<P> | null,
@@ -1891,7 +1892,7 @@ type FormStateActionQueueNode<P> = {
1892
function dispatchFormState<S, P>(
1893
fiber: Fiber,
1894
actionQueue: FormStateActionQueue<S, P>,
1894
- setState: Dispatch<Thenable<S>>,
1895
+ setState: Dispatch<S | Awaited<S>>,
1896
payload: P,
1897
): void {
1898
if (isRenderPhaseUpdate(fiber)) {
@@ -1907,7 +1908,7 @@ function dispatchFormState<S, P>(
1908
};
1909
newLast.next = actionQueue.pending = newLast;
1910
1910
- runFormStateAction(actionQueue, setState, payload);
1911
+ runFormStateAction(actionQueue, (setState: any), payload);
1912
} else {
1913
// There's already an action running. Add to the queue.
1914
const first = last.next;
@@ -1921,7 +1922,7 @@ function dispatchFormState<S, P>(
1922
1923
function runFormStateAction<S, P>(
1924
actionQueue: FormStateActionQueue<S, P>,
1924
- setState: Dispatch<Thenable<S>>,
1925
+ setState: Dispatch<S | Awaited<S>>,
1926
payload: P,
1927
) {
1928
const action = actionQueue.action;
@@ -1935,39 +1936,49 @@ function runFormStateAction<S, P>(
1936
ReactCurrentBatchConfig.transition._updatedFibers = new Set();
1937
}
1938
try {
1938
- const promise = action(prevState, payload);
1939
+ const returnValue = action(prevState, payload);
1940
+ if (
1941
+ returnValue !== null &&
1942
+ typeof returnValue === 'object' &&
1943
+ // $FlowFixMe[method-unbinding]
1944
+ typeof returnValue.then === 'function'
1945
+ ) {
1946
+ const thenable = ((returnValue: any): Thenable<Awaited<S>>);
1947
+
1948
+ // Attach a listener to read the return state of the action. As soon as
1949
+ // this resolves, we can run the next action in the sequence.
1950
+ thenable.then(
1951
+ (nextState: Awaited<S>) => {
1952
+ actionQueue.state = nextState;
1953
+ finishRunningFormStateAction(actionQueue, (setState: any));
1954
+ },
1955
+ () => finishRunningFormStateAction(actionQueue, (setState: any)),
1956
+ );
1957
1940
- if (__DEV__) {
1941
- if (
1942
- promise === null ||
1943
- typeof promise !== 'object' ||
1944
- typeof (promise: any).then !== 'function'
1945
- ) {
1946
- console.error(
1947
- 'The action passed to useFormState must be an async function.',
1948
- );
1949
- }
1958
+ const entangledResult = requestAsyncActionContext<S>(thenable, null);
1959
+ setState((entangledResult: any));
1960
+ } else {
1961
+ // This is either `returnValue` or a thenable that resolves to
1962
+ // `returnValue`, depending on whether we're inside an async action scope.
1963
+ const entangledResult = requestSyncActionContext<S>(returnValue, null);
1964
+ setState((entangledResult: any));
1965
+
1966
+ const nextState = ((returnValue: any): Awaited<S>);
1967
+ actionQueue.state = nextState;
1968
+ finishRunningFormStateAction(actionQueue, (setState: any));
1969
}
1951
-
1952
- // Attach a listener to read the return state of the action. As soon as this
1953
- // resolves, we can run the next action in the sequence.
1954
- promise.then(
1955
- (nextState: S) => {
1956
- actionQueue.state = nextState;
1957
- finishRunningFormStateAction(actionQueue, setState);
1958
- },
1959
- () => finishRunningFormStateAction(actionQueue, setState),
1960
- );
1961
-
1962
- // Create a thenable that resolves once the current async action scope has
1963
- // finished. Then stash that thenable in state. We'll unwrap it with the
1964
- // `use` algorithm during render. This is the same logic used
1965
- // by startTransition.
1966
- const entangledThenable: Thenable<S> = requestAsyncActionContext(
1967
- promise,
1968
- null,
1969
- );
1970
- setState(entangledThenable);
1970
+ } catch (error) {
1971
+ // This is a trick to get the `useFormState` hook to rethrow the error.
1972
+ // When it unwraps the thenable with the `use` algorithm, the error
1973
+ // will be thrown.
1974
+ const rejectedThenable: S = ({
1975
+ then() {},
1976
+ status: 'rejected',
1977
+ reason: error,
1978
+ // $FlowFixMe: Not sure why this doesn't work
1979
+ }: RejectedThenable<Awaited<S>>);
1980
+ setState(rejectedThenable);
1981
+ finishRunningFormStateAction(actionQueue, (setState: any));
1982
} finally {
1983
ReactCurrentBatchConfig.transition = prevTransition;
1984
@@ -1989,7 +2000,7 @@ function runFormStateAction<S, P>(
2000
2001
function finishRunningFormStateAction<S, P>(
2002
actionQueue: FormStateActionQueue<S, P>,
1992
- setState: Dispatch<Thenable<S>>,
2003
+ setState: Dispatch<S | Awaited<S>>,
2004
) {
2005
// The action finished running. Pop it from the queue and run the next pending
2006
// action, if there are any.
@@ -2005,7 +2016,7 @@ function finishRunningFormStateAction<S, P>(
2016
last.next = next;
2017
2018
// Run the next action.
2008
- runFormStateAction(actionQueue, setState, next.payload);
2019
+ runFormStateAction(actionQueue, (setState: any), next.payload);
2020
}
2021
}
2022
}
@@ -2015,11 +2026,11 @@ function formStateReducer<S>(oldState: S, newState: S): S {
2026
}
2027
2028
function mountFormState<S, P>(
2018
- action: (S, P) => Promise<S>,
2019
- initialStateProp: S,
2029
+ action: (Awaited<S>, P) => S,
2030
+ initialStateProp: Awaited<S>,
2031
permalink?: string,
2021
-): [S, (P) => void] {
2022
- let initialState = initialStateProp;
2032
+): [Awaited<S>, (P) => void] {
2033
+ let initialState: Awaited<S> = initialStateProp;
2034
if (getIsHydrating()) {
2035
const root: FiberRoot = (getWorkInProgressRoot(): any);
2036
const ssrFormState = root.formState;
@@ -2035,28 +2046,25 @@ function mountFormState<S, P>(
2046
}
2047
}
2048
}
2038
- const initialStateThenable: Thenable<S> = {
2039
- status: 'fulfilled',
2040
- value: initialState,
2041
- then() {},
2042
- };
2049
2050
// State hook. The state is stored in a thenable which is then unwrapped by
2051
// the `use` algorithm during render.
2052
const stateHook = mountWorkInProgressHook();
2047
- stateHook.memoizedState = stateHook.baseState = initialStateThenable;
2048
- const stateQueue: UpdateQueue<Thenable<S>, Thenable<S>> = {
2053
+ stateHook.memoizedState = stateHook.baseState = initialState;
2054
+ // TODO: Typing this "correctly" results in recursion limit errors
2055
+ // const stateQueue: UpdateQueue<S | Awaited<S>, S | Awaited<S>> = {
2056
+ const stateQueue = {
2057
pending: null,
2058
lanes: NoLanes,
2051
- dispatch: null,
2059
+ dispatch: (null: any),
2060
lastRenderedReducer: formStateReducer,
2053
- lastRenderedState: initialStateThenable,
2061
+ lastRenderedState: initialState,
2062
};
2063
stateHook.queue = stateQueue;
2056
- const setState: Dispatch<Thenable<S>> = (dispatchSetState.bind(
2064
+ const setState: Dispatch<S | Awaited<S>> = (dispatchSetState.bind(
2065
null,
2066
currentlyRenderingFiber,
2059
- stateQueue,
2067
+ ((stateQueue: any): UpdateQueue<S | Awaited<S>, S | Awaited<S>>),
2068
): any);
2069
stateQueue.dispatch = setState;
2070
@@ -2072,7 +2080,7 @@ function mountFormState<S, P>(
2080
pending: null,
2081
};
2082
actionQueueHook.queue = actionQueue;
2075
- const dispatch = dispatchFormState.bind(
2083
+ const dispatch = (dispatchFormState: any).bind(
2084
null,
2085
currentlyRenderingFiber,
2086
actionQueue,
@@ -2089,10 +2097,10 @@ function mountFormState<S, P>(
2097
}
2098
2099
function updateFormState<S, P>(
2092
- action: (S, P) => Promise<S>,
2093
- initialState: S,
2100
+ action: (Awaited<S>, P) => S,
2101
+ initialState: Awaited<S>,
2102
permalink?: string,
2095
-): [S, (P) => void] {
2103
+): [Awaited<S>, (P) => void] {
2104
const stateHook = updateWorkInProgressHook();
2105
const currentStateHook = ((currentHook: any): Hook);
2106
return updateFormStateImpl(
@@ -2107,18 +2115,24 @@ function updateFormState<S, P>(
2115
function updateFormStateImpl<S, P>(
2116
stateHook: Hook,
2117
currentStateHook: Hook,
2110
- action: (S, P) => Promise<S>,
2111
- initialState: S,
2118
+ action: (Awaited<S>, P) => S,
2119
+ initialState: Awaited<S>,
2120
permalink?: string,
2113
-): [S, (P) => void] {
2114
- const [thenable] = updateReducerImpl<Thenable<S>, Thenable<S>>(
2121
+): [Awaited<S>, (P) => void] {
2122
+ const [actionResult] = updateReducerImpl<S | Thenable<S>, S | Thenable<S>>(
2123
stateHook,
2124
currentStateHook,
2125
formStateReducer,
2126
);
2127
2128
// This will suspend until the action finishes.
2121
- const state = useThenable(thenable);
2129
+ const state: Awaited<S> =
2130
+ typeof actionResult === 'object' &&
2131
+ actionResult !== null &&
2132
+ // $FlowFixMe[method-unbinding]
2133
+ typeof actionResult.then === 'function'
2134
+ ? useThenable(((actionResult: any): Thenable<Awaited<S>>))
2135
+ : (actionResult: any);
2136
2137
const actionQueueHook = updateWorkInProgressHook();
2138
const actionQueue = actionQueueHook.queue;
@@ -2141,16 +2155,16 @@ function updateFormStateImpl<S, P>(
2155
2156
function formStateActionEffect<S, P>(
2157
actionQueue: FormStateActionQueue<S, P>,
2144
- action: (S, P) => Promise<S>,
2158
+ action: (Awaited<S>, P) => S,
2159
): void {
2160
actionQueue.action = action;
2161
}
2162
2163
function rerenderFormState<S, P>(
2150
- action: (S, P) => Promise<S>,
2151
- initialState: S,
2164
+ action: (Awaited<S>, P) => S,
2165
+ initialState: Awaited<S>,
2166
permalink?: string,
2153
-): [S, (P) => void] {
2167
+): [Awaited<S>, (P) => void] {
2168
// Unlike useState, useFormState doesn't support render phase updates.
2169
// Also unlike useState, we need to replay all pending updates again in case
2170
// the passthrough value changed.
@@ -2173,8 +2187,7 @@ function rerenderFormState<S, P>(
2187
}
2188
2189
// This is a mount. No updates to process.
2176
- const thenable: Thenable<S> = stateHook.memoizedState;
2177
- const state = useThenable(thenable);
2190
+ const state: Awaited<S> = stateHook.memoizedState;
2191
2192
const actionQueueHook = updateWorkInProgressHook();
2193
const actionQueue = actionQueueHook.queue;
@@ -3725,10 +3738,10 @@ if (__DEV__) {
3738
useHostTransitionStatus;
3739
(HooksDispatcherOnMountInDEV: Dispatcher).useFormState =
3740
function useFormState<S, P>(
3728
- action: (S, P) => Promise<S>,
3729
- initialState: S,
3741
+ action: (Awaited<S>, P) => S,
3742
+ initialState: Awaited<S>,
3743
permalink?: string,
3731
- ): [S, (P) => void] {
3744
+ ): [Awaited<S>, (P) => void] {
3745
currentHookNameInDev = 'useFormState';
3746
mountHookTypesDev();
3747
return mountFormState(action, initialState, permalink);
@@ -3895,10 +3908,10 @@ if (__DEV__) {
3908
useHostTransitionStatus;
3909
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useFormState =
3910
function useFormState<S, P>(
3898
- action: (S, P) => Promise<S>,
3899
- initialState: S,
3911
+ action: (Awaited<S>, P) => S,
3912
+ initialState: Awaited<S>,
3913
permalink?: string,
3901
- ): [S, (P) => void] {
3914
+ ): [Awaited<S>, (P) => void] {
3915
currentHookNameInDev = 'useFormState';
3916
updateHookTypesDev();
3917
return mountFormState(action, initialState, permalink);
@@ -4067,10 +4080,10 @@ if (__DEV__) {
4080
useHostTransitionStatus;
4081
(HooksDispatcherOnUpdateInDEV: Dispatcher).useFormState =
4082
function useFormState<S, P>(
4070
- action: (S, P) => Promise<S>,
4071
- initialState: S,
4083
+ action: (Awaited<S>, P) => S,
4084
+ initialState: Awaited<S>,
4085
permalink?: string,
4073
- ): [S, (P) => void] {
4086
+ ): [Awaited<S>, (P) => void] {
4087
currentHookNameInDev = 'useFormState';
4088
updateHookTypesDev();
4089
return updateFormState(action, initialState, permalink);
@@ -4239,10 +4252,10 @@ if (__DEV__) {
4252
useHostTransitionStatus;
4253
(HooksDispatcherOnRerenderInDEV: Dispatcher).useFormState =
4254
function useFormState<S, P>(
4242
- action: (S, P) => Promise<S>,
4243
- initialState: S,
4255
+ action: (Awaited<S>, P) => S,
4256
+ initialState: Awaited<S>,
4257
permalink?: string,
4245
- ): [S, (P) => void] {
4258
+ ): [Awaited<S>, (P) => void] {
4259
currentHookNameInDev = 'useFormState';
4260
updateHookTypesDev();
4261
return rerenderFormState(action, initialState, permalink);
@@ -4432,10 +4445,10 @@ if (__DEV__) {
4445
useHostTransitionStatus;
4446
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useFormState =
4447
function useFormState<S, P>(
4435
- action: (S, P) => Promise<S>,
4436
- initialState: S,
4448
+ action: (Awaited<S>, P) => S,
4449
+ initialState: Awaited<S>,
4450
permalink?: string,
4438
- ): [S, (P) => void] {
4451
+ ): [Awaited<S>, (P) => void] {
4452
currentHookNameInDev = 'useFormState';
4453
warnInvalidHookAccess();
4454
mountHookTypesDev();
@@ -4630,10 +4643,10 @@ if (__DEV__) {
4643
useHostTransitionStatus;
4644
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useFormState =
4645
function useFormState<S, P>(
4633
- action: (S, P) => Promise<S>,
4634
- initialState: S,
4646
+ action: (Awaited<S>, P) => S,
4647
+ initialState: Awaited<S>,
4648
permalink?: string,
4636
- ): [S, (P) => void] {
4649
+ ): [Awaited<S>, (P) => void] {
4650
currentHookNameInDev = 'useFormState';
4651
warnInvalidHookAccess();
4652
updateHookTypesDev();
@@ -4828,10 +4841,10 @@ if (__DEV__) {
4841
useHostTransitionStatus;
4842
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useFormState =
4843
function useFormState<S, P>(
4831
- action: (S, P) => Promise<S>,
4832
- initialState: S,
4844
+ action: (Awaited<S>, P) => S,
4845
+ initialState: Awaited<S>,
4846
permalink?: string,
4834
- ): [S, (P) => void] {
4847
+ ): [Awaited<S>, (P) => void] {
4848
currentHookNameInDev = 'useFormState';
4849
warnInvalidHookAccess();
4850
updateHookTypesDev();
packages/react-reconciler/src/ReactInternalTypes.js
+4
-3
@@ -15,6 +15,7 @@ import type {
15
Wakeable,
16
Usable,
17
ReactFormState,
18
+ Awaited,
19
} from 'shared/ReactTypes';
20
import type {WorkTag} from './ReactWorkTags';
21
import type {TypeOfMode} from './ReactTypeOfMode';
@@ -418,10 +419,10 @@ export type Dispatcher = {
419
reducer: ?(S, A) => S,
420
) => [S, (A) => void],
421
useFormState?: <S, P>(
421
- action: (S, P) => Promise<S>,
422
- initialState: S,
422
+ action: (Awaited<S>, P) => S,
423
+ initialState: Awaited<S>,
424
permalink?: string,
424
- ) => [S, (P) => void],
425
+ ) => [Awaited<S>, (P) => void],
426
};
427
428
export type CacheDispatcher = {
packages/react-reconciler/src/__tests__/ReactNewContext-test.js
+14
-5
@@ -1543,11 +1543,20 @@ describe('ReactNewContext', () => {
1543
}
1544
1545
function Root(props) {
1546
- return contextKeys.reduceRight((children, key) => {
1547
- const Context = contexts.get(key);
1548
- const value = props.values[key];
1549
- return <Context.Provider value={value}>{children}</Context.Provider>;
1550
- }, <ConsumerTree rand={props.rand} depth={0} maxDepth={props.maxDepth} />);
1546
+ return contextKeys.reduceRight(
1547
+ (children, key) => {
1548
+ const Context = contexts.get(key);
1549
+ const value = props.values[key];
1550
+ return (
1551
+ <Context.Provider value={value}>{children}</Context.Provider>
1552
+ );
1553
+ },
1554
+ <ConsumerTree
1555
+ rand={props.rand}
1556
+ depth={0}
1557
+ maxDepth={props.maxDepth}
1558
+ />,
1559
+ );
1560
}
1561
1562
const initialValues = contextKeys.reduce(
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js
+33
-39
@@ -347,14 +347,14 @@ describe('ReactFlightDOMForm', () => {
347
// @gate enableFormActions
348
// @gate enableAsyncActions
349
it("useFormState's dispatch binds the initial state to the provided action", async () => {
350
- const serverAction = serverExports(async function action(
351
- prevState,
352
- formData,
353
- ) {
354
- return {
355
- count: prevState.count + parseInt(formData.get('incrementAmount'), 10),
356
- };
357
- });
350
+ const serverAction = serverExports(
351
+ async function action(prevState, formData) {
352
+ return {
353
+ count:
354
+ prevState.count + parseInt(formData.get('incrementAmount'), 10),
355
+ };
356
+ },
357
+ );
358
359
const initialState = {count: 1};
360
function Client({action}) {
@@ -392,12 +392,11 @@ describe('ReactFlightDOMForm', () => {
392
// @gate enableFormActions
393
// @gate enableAsyncActions
394
it('useFormState can reuse state during MPA form submission', async () => {
395
- const serverAction = serverExports(async function action(
396
- prevState,
397
- formData,
398
- ) {
399
- return prevState + 1;
400
- });
395
+ const serverAction = serverExports(
396
+ async function action(prevState, formData) {
397
+ return prevState + 1;
398
+ },
399
+ );
400
401
function Form({action}) {
402
const [count, dispatch] = useFormState(action, 1);
@@ -481,13 +480,11 @@ describe('ReactFlightDOMForm', () => {
480
'useFormState preserves state if arity is the same, but different ' +
481
'arguments are bound (i.e. inline closure)',
482
async () => {
484
- const serverAction = serverExports(async function action(
485
- stepSize,
486
- prevState,
487
- formData,
488
- ) {
489
- return prevState + stepSize;
490
- });
483
+ const serverAction = serverExports(
484
+ async function action(stepSize, prevState, formData) {
485
+ return prevState + stepSize;
486
+ },
487
+ );
488
489
function Form({action}) {
490
const [count, dispatch] = useFormState(action, 1);
@@ -597,19 +594,17 @@ describe('ReactFlightDOMForm', () => {
594
it('useFormState does not reuse state if action signatures are different', async () => {
595
// This is the same as the previous test, except instead of using bind to
596
// configure the server action (i.e. a closure), it swaps the action.
600
- const increaseBy1 = serverExports(async function action(
601
- prevState,
602
- formData,
603
- ) {
604
- return prevState + 1;
605
- });
597
+ const increaseBy1 = serverExports(
598
+ async function action(prevState, formData) {
599
+ return prevState + 1;
600
+ },
601
+ );
602
607
- const increaseBy5 = serverExports(async function action(
608
- prevState,
609
- formData,
610
- ) {
611
- return prevState + 5;
612
- });
603
+ const increaseBy5 = serverExports(
604
+ async function action(prevState, formData) {
605
+ return prevState + 5;
606
+ },
607
+ );
608
609
function Form({action}) {
610
const [count, dispatch] = useFormState(action, 1);
@@ -680,12 +675,11 @@ describe('ReactFlightDOMForm', () => {
675
// @gate enableFormActions
676
// @gate enableAsyncActions
677
it('when permalink is provided, useFormState compares that instead of the keypath', async () => {
683
- const serverAction = serverExports(async function action(
684
- prevState,
685
- formData,
686
- ) {
687
- return prevState + 1;
688
- });
678
+ const serverAction = serverExports(
679
+ async function action(prevState, formData) {
680
+ return prevState + 1;
681
+ },
682
+ );
683
684
function Form({action, permalink}) {
685
const [count, dispatch] = useFormState(action, 1, permalink);
packages/react-server/src/ReactFizzHooks.js
+4
-3
@@ -15,6 +15,7 @@ import type {
15
Thenable,
16
Usable,
17
ReactCustomFormAction,
18
+ Awaited,
19
} from 'shared/ReactTypes';
20
21
import type {ResumableState} from './ReactFizzConfig';
@@ -612,10 +613,10 @@ function createPostbackFormStateKey(
613
}
614
615
function useFormState<S, P>(
615
- action: (S, P) => Promise<S>,
616
- initialState: S,
616
+ action: (Awaited<S>, P) => S,
617
+ initialState: Awaited<S>,
618
permalink?: string,
618
-): [S, (P) => void] {
619
+): [Awaited<S>, (P) => void] {
620
resolveCurrentlyRenderingComponent();
621
622
// Count the number of useFormState hooks per component. We also use this to
packages/react/src/__tests__/forwardRef-test.js
+5
-6
@@ -86,12 +86,11 @@ describe('forwardRef', () => {
86
);
87
}
88
89
- const RefForwardingComponent = React.forwardRef(function NamedFunction(
90
- props,
91
- ref,
92
- ) {
93
- return <FunctionComponent {...props} forwardedRef={ref} />;
94
- });
89
+ const RefForwardingComponent = React.forwardRef(
90
+ function NamedFunction(props, ref) {
91
+ return <FunctionComponent {...props} forwardedRef={ref} />;
92
+ },
93
+ );
94
RefForwardingComponent.propTypes = {
95
optional: PropTypes.string,
96
required: PropTypes.string.isRequired,
packages/shared/ReactTypes.js
+10
@@ -184,3 +184,13 @@ export type ReactFormState<S, ReferenceId> = [
184
ReferenceId /* Server Reference ID */,
185
number /* number of bound arguments */,
186
];
187
+
188
+export type Awaited<T> = T extends null | void
189
+ ? T // special case for `null | undefined` when not in `--strictNullChecks` mode
190
+ : T extends Object // `await` only unwraps object types with a callable then. Non-object types are not unwrapped.
191
+ ? T extends {then(onfulfilled: infer F): any} // thenable, extracts the first argument to `then()`
192
+ ? F extends (value: infer V) => any // if the argument to `then` is callable, extracts the argument
193
+ ? Awaited<V> // recursively unwrap the value
194
+ : empty // the argument to `then` was not callable.
195
+ : T // argument was not an object
196
+ : T; // non-thenable
scripts/circleci/run_devtools_e2e_tests.js
+3
-4
@@ -11,10 +11,9 @@ const inlinePackagePath = join(ROOT_PATH, 'packages', 'react-devtools-inline');
11
const shellPackagePath = join(ROOT_PATH, 'packages', 'react-devtools-shell');
12
const screenshotPath = join(ROOT_PATH, 'tmp', 'screenshots');
13
14
-const {SUCCESSFUL_COMPILATION_MESSAGE} = require(join(
15
- shellPackagePath,
16
- 'constants.js'
17
-));
14
+const {SUCCESSFUL_COMPILATION_MESSAGE} = require(
15
+ join(shellPackagePath, 'constants.js')
16
+);
17
18
let buildProcess = null;
19
let serverProcess = null;
scripts/error-codes/invertObject.js
+1
-1
@@ -13,7 +13,7 @@
13
* { 0: 'MUCH ERROR', 1: 'SUCH WRONG' }
14
*/
15
function invertObject(targetObj) {
16
- const result /*: {[string]: string} */ = {};
16
+ const result = {};
17
const mapKeys = Object.keys(targetObj);
18
19
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
scripts/prettier/index.js
+47
-37
@@ -22,8 +22,6 @@ const shouldWrite = mode === 'write' || mode === 'write-changed';
22
const onlyChanged = mode === 'check-changed' || mode === 'write-changed';
23
24
const changedFiles = onlyChanged ? listChangedFiles() : null;
25
-let didWarn = false;
26
-let didError = false;
25
26
const prettierIgnoreFilePath = path.join(
27
__dirname,
@@ -66,44 +64,56 @@ if (!files.length) {
64
process.exit(0);
65
}
66
69
-files.forEach(file => {
70
- const options = prettier.resolveConfig.sync(file, {
71
- config: prettierConfigPath,
72
- });
73
- try {
74
- const input = fs.readFileSync(file, 'utf8');
75
- if (shouldWrite) {
76
- const output = prettier.format(input, options);
77
- if (output !== input) {
78
- fs.writeFileSync(file, output, 'utf8');
79
- }
80
- } else {
81
- if (!prettier.check(input, options)) {
82
- if (!didWarn) {
83
- console.log(
84
- '\n' +
85
- chalk.red(
86
- ` This project uses prettier to format all JavaScript code.\n`
87
- ) +
88
- chalk.dim(` Please run `) +
89
- chalk.reset('yarn prettier-all') +
90
- chalk.dim(
91
- ` and add changes to files listed below to your commit:`
92
- ) +
93
- `\n\n`
94
- );
95
- didWarn = true;
67
+async function main() {
68
+ let didWarn = false;
69
+ let didError = false;
70
+
71
+ await Promise.all(
72
+ files.map(async file => {
73
+ const options = await prettier.resolveConfig(file, {
74
+ config: prettierConfigPath,
75
+ });
76
+ try {
77
+ const input = fs.readFileSync(file, 'utf8');
78
+ if (shouldWrite) {
79
+ const output = await prettier.format(input, options);
80
+ if (output !== input) {
81
+ fs.writeFileSync(file, output, 'utf8');
82
+ }
83
+ } else {
84
+ const isFormatted = await prettier.check(input, options);
85
+ if (!isFormatted) {
86
+ if (!didWarn) {
87
+ console.log(
88
+ '\n' +
89
+ chalk.red(
90
+ ` This project uses prettier to format all JavaScript code.\n`
91
+ ) +
92
+ chalk.dim(` Please run `) +
93
+ chalk.reset('yarn prettier-all') +
94
+ chalk.dim(
95
+ ` and add changes to files listed below to your commit:`
96
+ ) +
97
+ `\n\n`
98
+ );
99
+ didWarn = true;
100
+ }
101
+ console.log(file);
102
+ }
103
}
104
+ } catch (error) {
105
+ didError = true;
106
+ console.log('\n\n' + error.message);
107
console.log(file);
108
}
99
- }
100
- } catch (error) {
101
- didError = true;
102
- console.log('\n\n' + error.message);
103
- console.log(file);
109
+ })
110
+ );
111
+ if (didWarn || didError) {
112
+ process.exit(1);
113
}
105
-});
114
+}
115
107
-if (didWarn || didError) {
116
+main().catch(error => {
117
+ console.error(error);
118
process.exit(1);
109
-}
119
+});
scripts/print-warnings/print-warnings.js
+5
@@ -89,6 +89,11 @@ gs([
89
'!**/__tests__/**/*.js',
90
'!**/__mocks__/**/*.js',
91
'!**/node_modules/**/*.js',
92
+ // TODO: The newer Flow type syntax in this file breaks the parser and I can't
93
+ // figure out how to get Babel to parse it. I wasted too much time on
94
+ // something so unimportant so I'm skipping this for now. There's no actual
95
+ // code or warnings in this file anyway.
96
+ '!packages/shared/ReactTypes.js',
97
]).pipe(
98
through.obj(transform, cb => {
99
process.stdout.write(Array.from(warnings).sort().join('\n') + '\n');
scripts/rollup/generate-inline-fizz-runtime.js
+1
-1
@@ -88,7 +88,7 @@ async function main() {
88
(_, variableName) => variableName
89
);
90
91
- const prettyOutputCode = prettier.format(outputCode, prettierConfig);
91
+ const prettyOutputCode = await prettier.format(outputCode, prettierConfig);
92
93
fs.writeFileSync(inlineCodeStringsFilename, prettyOutputCode, 'utf8');
94
}
scripts/shared/evalToString.js
+2
-7
@@ -3,12 +3,10 @@
3
*
4
* This source code is licensed under the MIT license found in the
5
* LICENSE file in the root directory of this source tree.
6
- *
7
- * @flow
6
*/
7
'use strict';
8
11
-function evalStringConcat(ast /*: Object */) /*: string */ {
9
+function evalStringConcat(ast) {
10
switch (ast.type) {
11
case 'StringLiteral':
12
case 'Literal': // ESLint
@@ -24,10 +22,7 @@ function evalStringConcat(ast /*: Object */) /*: string */ {
22
}
23
exports.evalStringConcat = evalStringConcat;
24
27
-function evalStringAndTemplateConcat(
28
- ast /*: Object */,
29
- args /*: Array<mixed> */
30
-) /*: string */ {
25
+function evalStringAndTemplateConcat(ast, args) {
26
switch (ast.type) {
27
case 'StringLiteral':
28
return ast.value;
yarn.lock
+30
-11
@@ -2379,6 +2379,11 @@
2379
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
2380
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
2381
2382
+"@jridgewell/sourcemap-codec@^1.4.15":
2383
+ version "1.4.15"
2384
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
2385
+ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
2386
+
2387
"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.9":
2388
version "0.3.17"
2389
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985"
@@ -3022,7 +3027,14 @@
3027
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.14.tgz#5465ce598486a703caddbefe8603f8a2cffa3461"
3028
integrity sha512-wvzClDGQXOCVNU4APPopC2KtMYukaF1MN/W3xAmslx22Z4/IF1/izDMekuyoUlwfnDHYCIZGaj7jMwnJKBTxKw==
3029
3025
-"@types/prettier@^1.0.0 || ^2.0.0", "@types/prettier@^2.1.5":
3030
+"@types/prettier@^1.0.0 || ^2.0.0 || ^3.0.0":
3031
+ version "3.0.0"
3032
+ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-3.0.0.tgz#e9bc8160230d3a461dab5c5b41cceef1ef723057"
3033
+ integrity sha512-mFMBfMOz8QxhYVbuINtswBp9VL2b4Y0QqYHwqLz3YbgtfAcat2Dl6Y1o4e22S/OVE6Ebl9m7wWiMT2lSbAs1wA==
3034
+ dependencies:
3035
+ prettier "*"
3036
+
3037
+"@types/prettier@^2.1.5":
3038
version "2.7.2"
3039
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0"
3040
integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==
@@ -10869,6 +10881,13 @@ magic-string@0.26.7:
10881
dependencies:
10882
sourcemap-codec "^1.4.8"
10883
10884
+magic-string@0.30.5:
10885
+ version "0.30.5"
10886
+ resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
10887
+ integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==
10888
+ dependencies:
10889
+ "@jridgewell/sourcemap-codec" "^1.4.15"
10890
+
10891
magic-string@^0.27.0:
10892
version "0.27.0"
10893
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3"
@@ -12511,10 +12530,10 @@ prepend-http@^2.0.0:
12530
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
12531
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
12532
12514
-prettier@2.8.3:
12515
- version "2.8.3"
12516
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.3.tgz#ab697b1d3dd46fb4626fbe2f543afe0cc98d8632"
12517
- integrity sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==
12533
+prettier@*, prettier@3.0.3:
12534
+ version "3.0.3"
12535
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
12536
+ integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==
12537
12538
pretty-format@^27.2.5, pretty-format@^27.3.1:
12539
version "27.3.1"
@@ -13424,18 +13443,18 @@ roarr@^2.15.3:
13443
semver-compare "^1.0.0"
13444
sprintf-js "^1.1.2"
13445
13427
-rollup-plugin-prettier@^3.0.0:
13428
- version "3.0.0"
13429
- resolved "https://registry.yarnpkg.com/rollup-plugin-prettier/-/rollup-plugin-prettier-3.0.0.tgz#c208f31bc5ecef76ba69177bc9c1463667c9b19a"
13430
- integrity sha512-E0UqeVX1F+ATrHsXKXIywddjK+iFKOeOGI/drZY/wVq/xfHPjghviIhsFz7I0Wfuzp8jeN+4L7kVwQ/X84mOBw==
13446
+rollup-plugin-prettier@^4.1.1:
13447
+ version "4.1.1"
13448
+ resolved "https://registry.yarnpkg.com/rollup-plugin-prettier/-/rollup-plugin-prettier-4.1.1.tgz#eb74bd47c3cc3ba68bdf34b5323d0d7a47be8cec"
13449
+ integrity sha512-ugpi/EqW12yJa4NO3o4f/wt/YHwiQovVGC2jxZgxuKO9osjt4lVxVA427+itl87XmQc6089ZkpDc6OpaOZKWgQ==
13450
dependencies:
13432
- "@types/prettier" "^1.0.0 || ^2.0.0"
13451
+ "@types/prettier" "^1.0.0 || ^2.0.0 || ^3.0.0"
13452
diff "5.1.0"
13453
lodash.hasin "4.5.2"
13454
lodash.isempty "4.4.0"
13455
lodash.isnil "4.0.0"
13456
lodash.omitby "4.6.0"
13438
- magic-string "0.26.7"
13457
+ magic-string "0.30.5"
13458
13459
rollup-plugin-strip-banner@^3.0.0:
13460
version "3.0.0"