Skip to content

fix: correctly and fully SSR on deferred fragments#28

Merged
XiNiHa merged 1 commit into
mainfrom
ssr-stable-data
May 29, 2025
Merged

fix: correctly and fully SSR on deferred fragments#28
XiNiHa merged 1 commit into
mainfrom
ssr-stable-data

Conversation

@XiNiHa

@XiNiHa XiNiHa commented May 29, 2025

Copy link
Copy Markdown
Owner

This PR fixes SSR issues on deferred fragments caused by Solid caching source params of createResource(), which leads to a stale closures issue.

The solution is not to make the closures stale, by:

  • Caching data store instances keyed with resource references
  • Not relying on reactive primitives on the server

This fix wouldn't have been necessary if solidjs/solid#2497 had been merged, but it'd be beneficial to also fix this on our side, as it'll allow the fix to work regardless of the Solid version used.

@changeset-bot

changeset-bot Bot commented May 29, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 30c3892

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
solid-relay b4b5dfa Visit preview May 29 2025, 10:57 AM

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hello @XiNiHa, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

Summary of Changes

Hello! Gemini or gemini-code-assist here, providing a summary of this pull request.

This PR addresses a server-side rendering (SSR) issue specifically related to deferred fragments in solid-relay. The core problem identified is that Solid's createResource mechanism, when used in this context, caches the parameters provided to its source function. This caching behavior, particularly during SSR, can lead to stale closures, meaning the resource might hold onto outdated references to reactive primitives or context values when the fragment key changes.

To fix this, the PR introduces a new approach: caching the Solid store instances themselves. These stores, which hold the fragment or query data, are now keyed by the createResource instance they are associated with, using a WeakMap stored in the Relay environment context. This ensures that when a resource re-runs (e.g., due to a key change), it either retrieves the correct, existing store instance or creates a new one, preventing stale data from being displayed or used during SSR and subsequent hydration. Additionally, the implementation avoids relying on reactive primitives specifically on the server where their behavior differs. The PR also refactors the data access pattern by replacing the custom DataProxy utility with a new DataStore utility that incorporates this caching logic and streamlines how data, loading state, and errors are exposed.

Highlights

  • Fix SSR for Deferred Fragments: The primary goal is to resolve issues where deferred fragments were not correctly or fully rendering on the server due to how Solid's createResource interacted with reactive data sources.
  • Address Stale Closures: The fix targets a specific problem where createResource caching its source parameters leads to stale closures, causing incorrect data handling when fragment keys change, particularly during SSR.
  • Introduce DataStore Utility: A new utility, DataStore, is introduced to manage the state (data, error, pending) and provide a consistent interface for accessing data from fragments and queries, replacing the previous DataProxy.
  • Cache Store Instances: A WeakMap is added to the Relay environment context to cache and reuse Solid store instances based on their associated createResource instance. This ensures updates are applied to the correct store, preventing stale data.
  • Refactor Primitives: The core primitives (createFragment, createLazyLoadQuery, createPaginationFragment, createPreloadedQuery) are updated to use the new DataStore utility and leverage the store caching mechanism.

Changelog

Click here to see the changelog
  • .changeset/fast-plants-dress.md
    • Add new changeset file for a minor fix.
  • src/RelayEnvironment.ts
    • Import Resource type from 'solid-js' (line 5).
    • Add dataStores WeakMap to the RelayContext type (line 20).
    • Initialize dataStores WeakMap in the RelayEnvironmentProvider context value (line 28).
    • Add useDataStores hook to access the dataStores WeakMap from context (lines 52-57).
  • src/primitives/createFragment.ts
    • Update imports to include Subscription, createSignal, untrack, SetStoreFunction, isServer, DataStore, createDataStore and remove unused imports (createComputed, createMemo, onCleanup, DataProxy, makeDataProxy) (lines 5, 8-11).
    • Change return types of createFragment and createFragmentInternal from DataProxy to DataStore (lines 36, 40, 44, 54).
    • Move and define initialResult earlier (lines 57-61).
    • Define resultUpdateObserver object to handle subscription next events (lines 66-89).
    • Add subscription signal to manage the fragment subscription (line 90).
    • Add setResultQueue and initially set setResult to queue updates before the store is created (lines 92-97).
    • Refactor createResource source function to untrack subscription cleanup and reset state before evaluating the key (lines 101-110).
    • Refactor createResource async fetcher function to create the fragment source inside the async block, subscribe using the resultUpdateObserver, resolve/reject based on state, and unsubscribe on the server (lines 113-141).
    • Add onHydrated option to createResource to re-subscribe after hydration (lines 144-151).
    • Remove the old createStore and createComputed logic for managing the store and subscription (lines 98-141 in old code).
    • Use the new createDataStore utility to create the store, passing the resource and initial value (lines 155-158).
    • Process any queued setResult calls and reassign setResult to the actual store setter (lines 159-162).
    • Return the DataStore proxy from createDataStore (line 164).
  • src/primitives/createLazyLoadQuery.ts
    • Update imports to include DataStore, createDataStore and remove unused imports (createSignal, createStore, makeDataProxy) (lines 23, 30).
    • Change return types of createLazyLoadQuery and createLazyLoadQueryInternal from DataProxy to DataStore (lines 57, 87).
    • Remove the serverData signal (line 90).
    • Change the type of entry in createResource fetcher from false to null (line 183).
    • Remove the updateData helper function (lines 230-234 in old code).
    • Use the new createDataStore utility to create the store, passing the initial value and resource accessor (lines 225-228).
    • Update the subscription handler logic to directly use setResult with reconcile for data updates (lines 247-250).
    • Update the subscription handler logic to use setResult for error state (line 252).
    • Return the DataStore proxy directly (line 264).
  • src/primitives/createPaginationFragment.ts
    • Change type DataProxy to DataStore in CreatePaginationFragmentReturn (line 46).
    • Change type DataProxy to DataStore for the fragmentData parameter in createLoadMore (line 172).
  • src/primitives/createPreloadedQuery.ts
    • Change return type of createPreloadedQuery from DataProxy to DataStore (line 21).
  • src/primitives/createRefetchableFragment.ts
    • Import isServer from 'solid-js/web' (line 33).
    • Change type DataProxy to DataStore in return types of createRefetchableFragment (lines 90, 98, 108) and createRefetchableFragmentInternal (line 126).
    • Update the call to createFragmentInternal to use parentFragmentRef on the server and fragmentRef on the client (line 273).
  • src/queryCache.ts
    • Import Resource type from 'solid-js' (line 2).
    • Update QueryCacheEntry type: change resource type from () => void to Resource<unknown> (line 6) and replace false with null (line 9).
  • src/utils/dataProxy.ts
    • Delete the dataProxy.ts file.
  • src/utils/dataStore.ts
    • Add new file dataStore.ts (lines 1-89).
    • Define the DataStore type (lines 6-24).
    • Export createDataStore function which creates and returns a DataStore proxy and its setter (lines 26-62).
    • Implement createStableStore function which uses a WeakMap from context to cache and retrieve store instances based on the associated Solid Resource (lines 64-79).
    • Implement extractResource helper function (lines 81-89).
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments to provide feedback.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.


Server renders fast,
Then client hydrates with care,
Data flows just right.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@XiNiHa XiNiHa force-pushed the ssr-stable-data branch from b4b5dfa to 30c3892 Compare May 29, 2025 11:07
@pkg-pr-new

pkg-pr-new Bot commented May 29, 2025

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/XiNiHa/solid-relay@28

commit: 30c3892

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and well-thought-out fix for server-side rendering (SSR) issues with deferred fragments, specifically addressing stale closure problems with Solid's createResource. The core idea of caching data store instances keyed by resource references and avoiding reactive primitives on the server where problematic is sound and effectively implemented.

The introduction of DataStore and createDataStore, along with the WeakMap-based caching in RelayEnvironmentProvider, forms a robust foundation for this fix. The changes in createFragment to manage its subscription lifecycle directly (especially unsubscribing on the server) and in createRefetchableFragment to use non-reactive refs on the server are key enablers.

Overall, this is a high-quality contribution that improves the reliability of SSR with solid-relay. The proactive approach to fixing this within the library, rather than solely relying on upstream changes in Solid, is commendable as it provides broader compatibility.

Summary of Findings

  • Complexity in createFragment state initialization: The setResultQueue mechanism in createFragment.ts introduces some complexity to manage state updates before the final store setter is available. While it appears to solve a potential timing issue, further clarification on its necessity and design trade-offs would be beneficial.
  • SSR Robustness: The changes significantly improve SSR robustness by implementing a caching layer for data stores and carefully managing reactive primitives and subscriptions on the server, particularly in createFragment and createRefetchableFragment.
  • Refactoring from DataProxy to DataStore: The replacement of DataProxy/makeDataProxy with DataStore/createDataStore is a key architectural change that centralizes the new caching logic and improves how data is handled by the primitives.

Merge Readiness

The pull request is in good shape and addresses a critical SSR issue effectively. The core changes are well-implemented.

Before merging, it would be beneficial to discuss the setResultQueue pattern in createFragment.ts (as noted in the review comment) to ensure full clarity on its design and necessity. Addressing this point would enhance the overall understanding and maintainability of this important primitive.

I am not authorized to approve pull requests, so please ensure further review and approval from authorized maintainers after considering the feedback.

Comment on lines +92 to +97
const setResultQueue: unknown[][] = [];
let setResult: SetStoreFunction<FragmentResult<TKey[" $data"]>> = (
...args: unknown[]
) => {
setResultQueue.push(args);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The introduction of setResultQueue and initializing setResult to a queuing function is an interesting pattern. This presumably handles updates that might occur (e.g., from the resource's source function or early parts of the fetcher) before createDataStore has fully initialized and returned the actual setStore function that setResult is later reassigned to.

Could you elaborate a bit on the necessity of this queuing mechanism? Was it primarily to address specific timing issues encountered with createResource's execution lifecycle in relation to the createDataStore (and its internal createStableStore) setup, especially when a store might be retrieved from the cache? Understanding the trade-offs or if alternative patterns were considered would be helpful for long-term maintainability.

@codecov

codecov Bot commented May 29, 2025

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 44.64286% with 93 lines in your changes missing coverage. Please review.

Project coverage is 27.85%. Comparing base (13cde64) to head (30c3892).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/primitives/createFragment.ts 4.59% 83 Missing ⚠️
src/primitives/createRefetchableFragment.ts 12.50% 7 Missing ⚠️
src/utils/dataStore.ts 96.07% 2 Missing ⚠️
src/primitives/createPreloadedQuery.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #28      +/-   ##
==========================================
+ Coverage   27.13%   27.85%   +0.72%     
==========================================
  Files          21       21              
  Lines        1360     1411      +51     
  Branches       65       70       +5     
==========================================
+ Hits          369      393      +24     
- Misses        991     1018      +27     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@XiNiHa XiNiHa merged commit 0f98f4d into main May 29, 2025
4 checks passed
@XiNiHa XiNiHa deleted the ssr-stable-data branch May 29, 2025 11:17
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.

1 participant