Skip to content

Conversation

grgbkr
Copy link
Contributor

@grgbkr grgbkr commented Jun 30, 2025

Starting from this PR contribution: #4474

Prior to this change the zero-solid integration created a new store whenever a query changed.
This caused everything depending on the store to be disposed and recreated, instead of receiving granular updates.
This PR ensures that every createQuery call only creates a store the first time and passes on the setter to the view, and the view is responsible for reconciling data when there's large changes.

  • Add a ZeroProvider and useZero so that the zero instance can be accessed through context, ZeroProvider can take a signal for a ZeroOptions or a Zero instance.
  • useZero return a signal of Zero instance since the signal to ZeroProvider can change the ZeroOptions or Zero instance.
  • Un-deprecate useQuery since it now requires context. Deprecates createQuery instead. useX is a good convention to communicate that this should be called inside a component and not outside.
  • Added createUseZero to prevent redundant annotations, and so the api mirror's React's apis much more closely.

Next steps:

  • Use solid's reconcile utility to achieve granular updates for they initial hydration of a changed query. This will require adding synthetic ids to rows.
  • Update docs

@grgbkr grgbkr requested a review from arv June 30, 2025 23:30
Copy link

vercel bot commented Jun 30, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
replicache-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jul 1, 2025 8:33pm
zbugs ✅ Ready (Inspect) Visit Preview Jul 1, 2025 8:33pm

Copy link

github-actions bot commented Jun 30, 2025

🐰 Bencher Report

Branchgrgbkr/solid-reconcile
TestbedLinux
Click to view all benchmark results
BenchmarkThroughputBenchmark Result
operations / second (ops/s) x 1e3
(Result Δ%)
Lower Boundary
operations / second (ops/s) x 1e3
(Limit %)
src/client/custom.bench.ts > big schema📈 view plot
🚷 view threshold
338.03 ops/s x 1e3
(-4.78%)Baseline: 355.02 ops/s x 1e3
327.67 ops/s x 1e3
(96.93%)
src/client/zero.bench.ts > basics > All 1000 rows x 10 columns (numbers)📈 view plot
🚷 view threshold
1.62 ops/s x 1e3
(+1.94%)Baseline: 1.59 ops/s x 1e3
1.55 ops/s x 1e3
(95.53%)
src/client/zero.bench.ts > pk compare > pk = N📈 view plot
🚷 view threshold
29.92 ops/s x 1e3
(-1.38%)Baseline: 30.34 ops/s x 1e3
29.54 ops/s x 1e3
(98.70%)
src/client/zero.bench.ts > with filter > Lower rows 500 x 10 columns (numbers)📈 view plot
🚷 view threshold
2.53 ops/s x 1e3
(+0.95%)Baseline: 2.51 ops/s x 1e3
2.44 ops/s x 1e3
(96.39%)
🐰 View full continuous benchmarking report in Bencher

@grgbkr
Copy link
Contributor Author

grgbkr commented Jul 1, 2025

FYI @devagrawal09

@devagrawal09
Copy link

Thanks for creating this PR. What's the plan for adding ids?
We don't need every row inside the view tree to be identifiable, we only need objects inside the same array to be identifiable among its siblings. it's okay for nested stuff to have the same ids.

Also is it possible that when a new query is constructed, the data returned from zero has the same references? We might not need ids if the data is always kept in memory by zero and reused across queries, because reconcile understands references and is able to key them automatically.

@arv
Copy link
Contributor

arv commented Jul 1, 2025

same references

We can definitely do same references. That might be simpler and more performant

Copy link

github-actions bot commented Jul 1, 2025

🐰 Bencher Report

Branchgrgbkr/solid-reconcile
TestbedLinux
Click to view all benchmark results
BenchmarkFile SizeBenchmark Result
kilobytes (KB)
(Result Δ%)
Upper Boundary
kilobytes (KB)
(Limit %)
zero-package.tgz📈 view plot
🚷 view threshold
1,209.32 KB
(+0.01%)Baseline: 1,209.18 KB
1,233.36 KB
(98.05%)
zero.js📈 view plot
🚷 view threshold
199.60 KB
(0.00%)Baseline: 199.60 KB
203.59 KB
(98.04%)
zero.js.br📈 view plot
🚷 view threshold
55.91 KB
(0.00%)Baseline: 55.91 KB
57.03 KB
(98.04%)
🐰 View full continuous benchmarking report in Bencher

"@rocicorp/eslint-config": "^0.7.0",
"@rocicorp/prettier-config": "^0.3.0",
"@solidjs/testing-library": "^0.8.10",
"jsdom": "^26.1.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? We are running the tests in a real browser, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed


const complete = {type: 'complete'} as const;
const unknown = {type: 'unknown'} as const;
export const COMPLETE: QueryResultDetails = {type: 'complete'} as const;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should freeze these to prevent more surprises

Copy link
Contributor Author

@grgbkr grgbkr Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. this actually modifies solid's behavior as it checks for frozen objects and copies them, but i think its best not to rely on it.

[this.#state, this.#setState] = createStore<State>([

this.#setState = setState;
const s = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove commented out logs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +690 to +696
const [state, setState] = createStore<State>([
{
'': undefined,
},
{type: 'unknown'},
]);
const data = () => state[0][''];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe const {data, state, setState} = createTestStore()

MD extends CustomMutatorDefs<S> | undefined = undefined,
>(props: {
children: JSX.Element;
zeroSignal: () => ZeroOptions<S, MD> | {zero: Zero<S, MD>};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this type a bit strange. I want the props to be called zero and options.

Is this how the React one is done?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed this to more closely match the React API. So its either:

<ZeroProvider zero={zero}>...</ZeroProvider>

or

<ZeroProvider server="https://foo.com" userID="u1" schema={schema}>...</ZeroProvider>

});
// eslint-disable-next-line @typescript-eslint/naming-convention
const MockZeroProvider = (props: {children: JSX.Element}) => (
<ZeroProvider zeroSignal={zeroOptions}>{props.children}</ZeroProvider>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<ZeroProvider zeroSignal={zeroOptions}>{props.children}</ZeroProvider>
<ZeroProvider options={zeroOptions}>{props.children}</ZeroProvider>

I hope

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i made this match the React version, so it would be something like:

<ZeroProvider server="https://foo.com" userID="u1" schema={schema}>

});
// eslint-disable-next-line @typescript-eslint/naming-convention
const MockZeroProvider = (props: {children: JSX.Element}) => (
<ZeroProvider zeroSignal={zeroSignal}>{props.children}</ZeroProvider>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<ZeroProvider zeroSignal={zeroSignal}>{props.children}</ZeroProvider>
<ZeroProvider zero={zeroSignal}>{props.children}</ZeroProvider>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


return ZeroContext.Provider({
value: zero,
get children() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so it seems this getter for children was the secret sauce... but why?

Why does the following not work

return ZeroContext.Provider({
  value: zero,
  children: props.children
  });
  

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should value also be a getter? There is still something fundamental about solidjs that i dont grok.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah you can't use JSX here

In that case the getter is correct.
value would also need to be a getter.

In solid when you access something from the props like props.value inside a reactive scope, it needs to behave similar to accessing a signal value() so that it can be tracked as a dependency. So the JSX transform changes <A b={c()} /> to A({ get b() { return c{} } }).

This makes sure that every property passed through the component tree can be tracked independently.

Copy link
Contributor

@arv arv Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

children needs to be a getter to get the state tracking.

In the case of value, zero is already a signal so there is no need to use a getter.

Comment on lines +71 to +82
return ZeroContext.Provider({
value: zero,
get children() {
return props.children;
},
});
Copy link

@devagrawal09 devagrawal09 Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return ZeroContext.Provider({
value: zero,
get children() {
return props.children;
},
});
return <ZeroContext.Provider value={zero()}>
{props.children}
</ZeroContext.Provider>

You should be able to do just this

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to avoid using JSX here as esbuild doesn't seem able to build a library that uses both React JSX and Solid JSX (at least we couldn't figure it out).

@grgbkr
Copy link
Contributor Author

grgbkr commented Jul 1, 2025

Thanks for creating this PR. What's the plan for adding ids? We don't need every row inside the view tree to be identifiable, we only need objects inside the same array to be identifiable among its siblings. it's okay for nested stuff to have the same ids.

Also is it possible that when a new query is constructed, the data returned from zero has the same references? We might not need ids if the data is always kept in memory by zero and reused across queries, because reconcile understands references and is able to key them automatically.

We do return the same references for rows that haven't changed. Let me do some testing and see if i can reconcile based on references working.

@grgbkr grgbkr force-pushed the grgbkr/solid-reconcile branch from 4d27167 to 40fe936 Compare July 1, 2025 20:32
@grgbkr grgbkr enabled auto-merge (squash) July 1, 2025 20:36
@grgbkr grgbkr merged commit 24d81c6 into main Jul 1, 2025
12 of 13 checks passed
@grgbkr grgbkr deleted the grgbkr/solid-reconcile branch July 1, 2025 20:39
@arv
Copy link
Contributor

arv commented Jul 2, 2025

We do return the same references for rows that haven't changed. Let me do some testing and see if i can reconcile based on references working.

We should also be able to "merge" the rows that changed if the primary keys didn't change. This can be dealt with in applyChange.

Copy link
Contributor

@arv arv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

});
createEffect(() => {
const details = resultDetails();
details.type;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no op?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it does set up the tracking?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah necessary to set up the tracking

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice and extensive tests!

return zero ?? (() => undefined);
}

export function createUseZero<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use some JSDoc.

@grgbkr
Copy link
Contributor Author

grgbkr commented Jul 3, 2025

Thanks for creating this PR. What's the plan for adding ids? We don't need every row inside the view tree to be identifiable, we only need objects inside the same array to be identifiable among its siblings. it's okay for nested stuff to have the same ids.
Also is it possible that when a new query is constructed, the data returned from zero has the same references? We might not need ids if the data is always kept in memory by zero and reused across queries, because reconcile understands references and is able to key them automatically.

We do return the same references for rows that haven't changed. Let me do some testing and see if i can reconcile based on references working.

@devagrawal09 @arv
The reference based approach isn't going to work due to some other work I'm doing to modify rows in place to achieve sub-row level precise reactivity. I got the idSymbol w synthetic id working. #4575

grgbkr added a commit that referenced this pull request Jul 4, 2025
… changes (#4575)

This is a follow up to: #4559

To effectively use solidjs's `reconcile` utility it was necessary to add a consistent identity key to every row in the view.
@arv
Copy link
Contributor

arv commented Jul 8, 2025

@devagrawal09 Should we file a bug/pr on Solid to allow the key to be a symbol?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants