@samitouri / QOS-React-1 / commits / 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.

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"