Skip to content

Commit cdfbc2e

Browse files
authored
Backport x v2 (#3780)
1 parent 53621d9 commit cdfbc2e

File tree

21 files changed

+376
-14
lines changed

21 files changed

+376
-14
lines changed

compat/src/index.d.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,23 @@ declare namespace React {
134134
): C;
135135

136136
export interface ForwardFn<P = {}, T = any> {
137-
(props: P, ref: Ref<T>): preact.ComponentChild;
137+
(props: P, ref: ForwardedRef<T>): preact.ComponentChild;
138138
displayName?: string;
139139
}
140140

141+
interface MutableRefObject<T> {
142+
current: T;
143+
}
144+
145+
export type ForwardedRef<T> =
146+
| ((instance: T | null) => void)
147+
| MutableRefObject<T | null>
148+
| null;
149+
150+
export type PropsWithChildren<P = unknown> = P & {
151+
children?: preact.ComponentChild | undefined;
152+
};
153+
141154
export function forwardRef<R, P = {}>(
142155
fn: ForwardFn<P, R>
143156
): preact.FunctionalComponent<Omit<P, 'ref'> & { ref?: preact.Ref<R> }>;

compat/src/index.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@ export function useTransition() {
137137
// styles/... before it attaches
138138
export const useInsertionEffect = useLayoutEffect;
139139

140+
/**
141+
* Check if two values are the same value
142+
* @param {*} x
143+
* @param {*} y
144+
* @returns {boolean}
145+
*/
146+
function is(x, y) {
147+
return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
148+
}
149+
140150
/**
141151
* This is taken from https://github.com/facebook/react/blob/main/packages/use-sync-external-store/src/useSyncExternalStoreShimClient.js#L84
142152
* on a high level this cuts out the warnings, ... and attempts a smaller implementation
@@ -152,18 +162,18 @@ export function useSyncExternalStore(subscribe, getSnapshot) {
152162
_instance._value = value;
153163
_instance._getSnapshot = getSnapshot;
154164

155-
if (_instance._value !== getSnapshot()) {
165+
if (!is(_instance._value, getSnapshot())) {
156166
forceUpdate({ _instance });
157167
}
158168
}, [subscribe, value, getSnapshot]);
159169

160170
useEffect(() => {
161-
if (_instance._value !== _instance._getSnapshot()) {
171+
if (!is(_instance._value, _instance._getSnapshot())) {
162172
forceUpdate({ _instance });
163173
}
164174

165175
return subscribe(() => {
166-
if (_instance._value !== _instance._getSnapshot()) {
176+
if (!is(_instance._value, _instance._getSnapshot())) {
167177
forceUpdate({ _instance });
168178
}
169179
});

compat/test/browser/hooks.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,40 @@ describe('React-18-hooks', () => {
130130
expect(scratch.innerHTML).to.equal('<p>hello new world</p>');
131131
});
132132

133+
it('getSnapshot can return NaN without causing infinite loop', () => {
134+
let flush;
135+
const subscribe = sinon.spy(cb => {
136+
flush = cb;
137+
return () => {};
138+
});
139+
let called = false;
140+
const getSnapshot = sinon.spy(() => {
141+
if (called) {
142+
return NaN;
143+
}
144+
145+
return 1;
146+
});
147+
148+
const App = () => {
149+
const value = useSyncExternalStore(subscribe, getSnapshot);
150+
return <p>{value}</p>;
151+
};
152+
153+
act(() => {
154+
render(<App />, scratch);
155+
});
156+
expect(scratch.innerHTML).to.equal('<p>1</p>');
157+
expect(subscribe).to.be.calledOnce;
158+
expect(getSnapshot).to.be.calledThrice;
159+
160+
called = true;
161+
flush();
162+
rerender();
163+
164+
expect(scratch.innerHTML).to.equal('<p>NaN</p>');
165+
});
166+
133167
it('should not call function values on subscription', () => {
134168
let flush;
135169
const subscribe = sinon.spy(cb => {

debug/src/index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* Reset the history of which prop type warnings have been logged.
3+
*/
4+
export function resetPropWarnings(): void;

hooks/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export function useReducer(reducer, initialState, init) {
216216
}
217217
});
218218

219-
return shouldUpdate
219+
return shouldUpdate || hookState._internal.props !== p
220220
? prevScu
221221
? prevScu.call(this, p, s, c)
222222
: true

hooks/test/browser/useState.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,29 @@ describe('useState', () => {
346346

347347
expect(renderSpy).to.be.calledTwice;
348348
});
349+
350+
// see preactjs/preact#3731
351+
it('respects updates initiated from the parent', () => {
352+
let setChild, setParent;
353+
const Child = props => {
354+
const [, setState] = useState(false);
355+
setChild = setState;
356+
return <p>{props.text}</p>;
357+
};
358+
359+
const Parent = () => {
360+
const [state, setState] = useState('hello world');
361+
setParent = setState;
362+
return <Child text={state} />;
363+
};
364+
365+
render(<Parent />, scratch);
366+
expect(scratch.innerHTML).to.equal('<p>hello world</p>');
367+
368+
setParent('hello world!!!');
369+
setChild(true);
370+
setChild(false);
371+
rerender();
372+
expect(scratch.innerHTML).to.equal('<p>hello world!!!</p>');
373+
});
349374
});

mangle.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"$_children": "__k",
5353
"$_pendingSuspensionCount": "__u",
5454
"$_childDidSuspend": "__c",
55+
"$_stateCallbacks": "_sb",
5556
"$_onResolve": "__R",
5657
"$_suspended": "__e",
5758
"$_dom": "__e",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"umd": "./compat/dist/compat.umd.js"
4545
},
4646
"./debug": {
47+
"types": "./debug/src/index.d.ts",
4748
"module": "./debug/dist/debug.mjs",
4849
"import": "./debug/dist/debug.mjs",
4950
"require": "./debug/dist/debug.js",

src/component.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Component.prototype.setState = function(update, callback) {
5353

5454
const internal = this._internal;
5555
if (update != null && internal) {
56-
if (callback) internal._commitCallbacks.push(callback.bind(this));
56+
if (callback) internal._stateCallbacks.push(callback.bind(this));
5757
internal.rerender(internal);
5858
}
5959
};

src/diff/mount.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,10 @@ function mountComponent(internal, startDom) {
367367
if (renderHook) renderHook(internal);
368368
if (ENABLE_CLASSES && internal.flags & TYPE_CLASS) {
369369
renderResult = c.render(c.props, c.state, c.context);
370+
for (let i = 0; i < internal._stateCallbacks.length; i++) {
371+
internal._commitCallbacks.push(internal._stateCallbacks[i]);
372+
}
373+
internal._stateCallbacks = [];
370374
// note: disable repeat render invocation for class components
371375
break;
372376
} else {

0 commit comments

Comments
 (0)