Skip to content

Commit 17763fa

Browse files
committed
fix #1023: Suspense top level track, fix #1032 reconcile top level key change
1 parent 5f929b0 commit 17763fa

File tree

5 files changed

+81
-31
lines changed

5 files changed

+81
-31
lines changed

packages/solid/src/render/Suspense.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export function Suspense(props: { fallback?: JSX.Element; children: JSX.Element
172172
return (flicker = undefined);
173173
}
174174
if (ctx && p === undefined) setHydrateContext();
175-
const rendered = untrack(() => props.children);
175+
const rendered = createMemo(() => props.children);
176176
return createMemo(() => {
177177
const inFallback = store.inFallback(),
178178
visibleContent = showContent ? showContent() : true,
@@ -182,7 +182,7 @@ export function Suspense(props: { fallback?: JSX.Element; children: JSX.Element
182182
store.resolved = true;
183183
ctx = p = undefined;
184184
resumeEffects(store.effects);
185-
return rendered;
185+
return rendered();
186186
}
187187
if (!visibleFallback) return;
188188
return createRoot(disposer => {

packages/solid/store/src/modifiers.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { batch } from "solid-js";
21
import { setProperty, unwrap, isWrappable, StoreNode, $RAW } from "./store";
32

3+
const $ROOT = Symbol("store-root")
4+
45
export type ReconcileOptions = {
56
key?: string | null;
67
merge?: boolean;
@@ -16,7 +17,10 @@ function applyState(
1617
const previous = parent[property];
1718
if (target === previous) return;
1819
if (!isWrappable(target) || !isWrappable(previous) || (key && target[key] !== previous[key])) {
19-
target !== previous && setProperty(parent, property, target);
20+
if (target !== previous) {
21+
if (property === $ROOT) return target;
22+
setProperty(parent, property, target);
23+
}
2024
return;
2125
}
2226

@@ -103,7 +107,7 @@ function applyState(
103107
}
104108
}
105109

106-
// Diff method for setState
110+
// Diff method for setStore
107111
export function reconcile<T extends U, U>(
108112
value: T,
109113
options: ReconcileOptions = {}
@@ -112,8 +116,8 @@ export function reconcile<T extends U, U>(
112116
v = unwrap(value);
113117
return state => {
114118
if (!isWrappable(state) || !isWrappable(v)) return v;
115-
batch(() => applyState(v, { state }, "state", merge, key));
116-
return state as T;
119+
const res = applyState(v, { [$ROOT]: state }, $ROOT, merge, key);
120+
return res === undefined ? state as T : res as T;
117121
};
118122
}
119123

packages/solid/store/src/server.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,23 @@ function mergeStoreNode(state: any, value: any, force?: boolean) {
2929
}
3030
}
3131

32+
function updateArray(
33+
current: any,
34+
next: Array<any> | Record<string, any> | ((prev: any) => Array<any> | Record<string, any>)
35+
) {
36+
if (typeof next === "function") next = next(current);
37+
if (Array.isArray(next)) {
38+
if (current === next) return;
39+
let i = 0,
40+
len = next.length;
41+
for (; i < len; i++) {
42+
const value = next[i];
43+
if (current[i] !== value) setProperty(current, i, value);
44+
}
45+
setProperty(current, "length", len);
46+
} else mergeStoreNode(current, next);
47+
}
48+
3249
export function updatePath(current: any, path: any[], traversed: PropertyKey[] = []) {
3350
let part,
3451
next = current;
@@ -75,8 +92,9 @@ export function updatePath(current: any, path: any[], traversed: PropertyKey[] =
7592
}
7693

7794
export function createStore<T>(state: T | Store<T>): [Store<T>, SetStoreFunction<T>] {
95+
const isArray = Array.isArray(state);
7896
function setStore(...args: any[]): void {
79-
updatePath(state, args);
97+
isArray && args.length === 1 ? updateArray(state, args[0]) : updatePath(state, args);
8098
}
8199
return [state as Store<T>, setStore];
82100
}

packages/solid/store/test/modifiers.spec.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
import { createRoot, createSignal, createEffect } from "../../src";
2-
import {
3-
createStore,
4-
createMutable,
5-
reconcile,
6-
produce,
7-
unwrap,
8-
modifyMutable
9-
} from "../src";
2+
import { createStore, createMutable, reconcile, produce, unwrap, modifyMutable } from "../src";
103

114
describe("setState with reconcile", () => {
125
test("Reconcile a simple object", () => {
@@ -98,6 +91,30 @@ describe("setState with reconcile", () => {
9891
expect(state.users[2].id).toBe(3);
9992
expect(state.users[2].firstName).toBe("Brandon");
10093
});
94+
95+
test("Reconcile top level key mismatch", () => {
96+
const JOHN = { id: 1, firstName: "John", lastName: "Snow" },
97+
NED = { id: 2, firstName: "Ned", lastName: "Stark" };
98+
99+
const [user, setUser] = createStore(JOHN);
100+
expect(user.id).toBe(1);
101+
expect(user.firstName).toBe("John");
102+
setUser(reconcile(NED));
103+
expect(user.id).toBe(2);
104+
expect(user.firstName).toBe("Ned");
105+
});
106+
107+
test("Reconcile nested top level key mismatch", () => {
108+
const JOHN = { id: 1, firstName: "John", lastName: "Snow" },
109+
NED = { id: 2, firstName: "Ned", lastName: "Stark" };
110+
111+
const [user, setUser] = createStore({ user: JOHN });
112+
expect(user.user.id).toBe(1);
113+
expect(user.user.firstName).toBe("John");
114+
setUser("user", reconcile(NED));
115+
expect(user.user.id).toBe(2);
116+
expect(user.user.firstName).toBe("Ned");
117+
});
101118
});
102119

103120
describe("setState with produce", () => {

packages/solid/web/test/suspense.spec.tsx

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,30 @@ import "../../test/MessageChannel";
33
import { lazy, createSignal, createResource, useTransition, enableScheduling } from "../../src";
44
import { render, Suspense, SuspenseList } from "../src";
55

6-
76
global.queueMicrotask = setImmediate;
87
enableScheduling();
98

109
beforeEach(() => {
1110
jest.useFakeTimers();
12-
})
11+
});
1312
afterEach(() => {
1413
jest.useRealTimers();
15-
})
14+
});
15+
describe("Testing Basics", () => {
16+
test("Children are reactive", () => {
17+
let div = document.createElement("div");
18+
let increment: () => void;
19+
render(() => {
20+
const [count, setCount] = createSignal(0);
21+
increment = () => setCount(count() + 1);
22+
return <Suspense>{count()}</Suspense>;
23+
}, div);
24+
expect(div.innerHTML).toBe("0");
25+
increment!();
26+
expect(div.innerHTML).toBe("1");
27+
});
28+
});
1629
describe("Testing Suspense", () => {
17-
1830
let div = document.createElement("div"),
1931
disposer: () => void,
2032
resolvers: Function[] = [],
@@ -40,7 +52,7 @@ describe("Testing Suspense", () => {
4052
expect(div.innerHTML).toBe("Loading");
4153
});
4254

43-
test("Toggle Suspense control flow", async (done) => {
55+
test("Toggle Suspense control flow", async done => {
4456
for (const r of resolvers) r({ default: ChildComponent });
4557

4658
queueMicrotask(() => {
@@ -49,7 +61,7 @@ describe("Testing Suspense", () => {
4961
});
5062
});
5163

52-
test("Toggle with refresh transition", async (done) => {
64+
test("Toggle with refresh transition", async done => {
5365
const [pending, start] = useTransition();
5466
let finished = false;
5567

@@ -62,20 +74,20 @@ describe("Testing Suspense", () => {
6274
expect(div.innerHTML).toBe("Hi, .Hello ");
6375
expect(pending()).toBe(true);
6476
expect(finished).toBe(false);
65-
77+
6678
// Exhausts create-resource setTimeout
6779
jest.runAllTimers();
6880
// wait update suspence state
6981
await Promise.resolve();
70-
// wait update computation
82+
// wait update computation
7183
jest.runAllTicks();
7284
jest.runAllTimers();
7385
// wait write signal succ
74-
queueMicrotask(()=>{
86+
queueMicrotask(() => {
7587
expect(div.innerHTML).toBe("Hi, Jo.Hello Jo");
7688
expect(pending()).toBe(false);
7789
expect(finished).toBe(true);
78-
done()
90+
done();
7991
});
8092
jest.runAllTicks();
8193
});
@@ -159,11 +171,11 @@ describe("SuspenseList", () => {
159171
jest.advanceTimersByTime(110);
160172
await Promise.resolve();
161173
expect(div.innerHTML).toBe("<div>Loading 1</div><div>Loading 2</div><div>Loading 3</div>");
162-
174+
163175
jest.advanceTimersByTime(100);
164176
await Promise.resolve();
165177
expect(div.innerHTML).toBe("<div>A</div><div>B</div><div>Loading 3</div>");
166-
178+
167179
jest.advanceTimersByTime(100);
168180
await Promise.resolve();
169181
expect(div.innerHTML).toBe("<div>A</div><div>B</div><div>C</div>");
@@ -191,12 +203,11 @@ describe("SuspenseList", () => {
191203
jest.advanceTimersByTime(110);
192204
await Promise.resolve();
193205
expect(div.innerHTML).toBe("");
194-
206+
195207
jest.advanceTimersByTime(100);
196208
await Promise.resolve();
197209
expect(div.innerHTML).toBe("<div>A</div><div>B</div>");
198-
199-
210+
200211
jest.advanceTimersByTime(100);
201212
await Promise.resolve();
202213
expect(div.innerHTML).toBe("<div>A</div><div>B</div><div>C</div>");

0 commit comments

Comments
 (0)