Replies: 7 comments 22 replies
-
Cache code example is perhaps meant to return fetch result? const getUser = cache((id) => {
return await fetch(`/api/users/${id}`)
}, "users") // used as cache key + serialized arguments |
Beta Was this translation helpful? Give feedback.
-
What is the API to access a route's preload function if we want to call it? Experience shows that can be a route-specific assembly of multiple data preloads so it would be nice to keep it right there with the route. |
Beta Was this translation helpful? Give feedback.
-
Request not to assume all |
Beta Was this translation helpful? Give feedback.
-
So in your example you show invalidating a specific const myAction = action(async (data) => {
await doMutation(data);
return redirect("/", {
invalidate: [[getUser, data.id]] // keys to invalidate
}) // returns a response
}); I guess the id is automatically part of the key because of the parameters? Guessing you could also pass "users" instead of the const getUser = cache((id) => {
return (await fetch(`/api/users${id}`)).json()
}, "users") // used as cache key + serialized arguments |
Beta Was this translation helpful? Give feedback.
-
The current specification conflicts with standard form[method=get]. The omission of method imply in a get form, but as of now form[method=get] isn’t even touched by the router, thought it should, as it’s as much a navigation method as a link. I think the router shouldn’t be the one taking care of the form[action=server] transformation, it doesn’t seem a routing concern. If it provided a way for the targeted routes from a form post to deal with the request, it would be enough. |
Beta Was this translation helpful? Give feedback.
-
Basic ExampleSo my suggestion would be to change a few bits here and there: // app.tsx
// ...
render(
() => (
<Router>
<Route component={AppLayout}>
<Route path="/users/:id" component={User} preload={preloadUser} />
<Route
path="/users"
component={UsersList}
// 1st called
onPost={createUser}
// 2nd called if no redirect
preload={preloadUsers}
/>
<Route path="/" component={Home} />
</Route>
</Router>
),
document.getElementById("app")
); AffordanceAs much as links (a), forms manipulate the current location. In the case of As much as the action extraction is very useful it shouldn't overstep into the routing territory, neither change Browser semantics. The first RFC suggests to change form default method to POST whenever a action attribute is defined with an The action extraction can very well happen but without being constrained with by the routing and vice-versa. |
Beta Was this translation helpful? Give feedback.
-
@ryansolid is it possible to pause the rendering completely until |
Beta Was this translation helpful? Give feedback.
-
Summary
In hopes of aligning with a future where cache centric approach is to routing this RFC outlines a change in Router architecture to enable optimal data fetching patterns for CSR & SSR SPAs, MPAs, and future Hybrid solutions.
Basic Example
Motivation
Solid Router aims to be the universal router for all web applications for Solid. There are few really important evolutions that are taking us in this direction, that this RFC seeks to solve.
1. Link Preloading with Reactive data doesn't really work
One of the longest asked for features is having hover preloading and we've never delivered on it because it is messy at best. Mostly that without caching solution it has no benefit unless we persist the reactivity for period of time. This makes disposal complicated. It makes it hard to call the function more than once. We've mostly resolved that to do this the developer would need a cache solution anyway (like Tanstack Query) but the Router not offering that yet offering preloading is a bit at odds.
2. Data Functions Confuse Developers expecting Blocking Middleware
The best thing about our Data functions are they are not blocking by default. This really lets you leverage things like Suspense and Transitions in your application code. You should never really think of them like a single Loader but a mechanism to set up multiple loaders. We've had many requests over time to make them blocking or access Context that they could never have access to because they are hoisted. We've encouraged people to pull more work into them, and that is good, but it sets a false pretext for some. With Preload Functions I think this expectation can't exist. These are just optional functions that warm the cache. They aren't blocking. If the component beats you to fetching something that you have in preload so be it.
3. People like Fetching in Components
Now I'm not going to say people are right. No matter what if you fetch in your component without doing hoisted preloading you risk waterfalls for the unaware. Moreover hoisting all data fetching to the top-most component in a nested section is also not guaranteed to not waterfall given that the parent needs to render before child first for Context to work. So having a dedicated place to
preload
like we do forloaders
today is essential. Of course with techniques like "hover" preloading, avoiding wrapping nested sections in conditionals, and extreme vigilance, you could author an optimal app without preloading. And the naive approach is at least better than the naive approach today.However, giving people a place to preload and playing into these sort of patterns is much friendly to other 3rd party libraries. This aligns with things like Tanstack Query better.
4. Enforce better patterns for TypeScript/Data Layer
Things like
useRouteData
never really sat great with TypeScript. Moving towards a direct import approach withcache
andactions
will allow completions.These are also designed to work with our Server Functions both on the fetching and mutation side. There is an added benefit that pushing these references to be global so they can be referenced will discourage people defining server functions inline in components. This will remove need for ambiguous closure abstraction to prevent accidental leakage.
Simply by API design we should be able to avoid common footguns.
5. Hybrid Islands Routing/Server Components don't support Context
They can't. Once you navigate to the next page with HTML partials there is a chance for the server to render components that read from Context (or Global State) that has been updated in the client since the initial render. The server being unaware could not render the right thing and cause Hydration Mismatch errors. It is fundamental that these solutions always render components that use persistent client state on the client only after initial SSR. Also to support nested partials we can't assume that the parent will have executed, so each Route section must be responsible for its own data and dedupe via a cache.
6. A breaking change sooner is better than later with SolidStart in mind
Knowing what is coming for both Solid 2.0 and these new areas of Routing it probably makes sense to do the bigger changes sooner than later. SolidStart aims to not wrap other libraries(including the router) anymore so these APIs being proposed for Data are those you would choose to use in SolidStart if you choose to use this Router with it.
Detailed Design
I've written a mock ReadMe with all the updates if you prefer to see the proposal more from a consumer standpoint. You can see it here: https://hackmd.io/@0u1u3zEAQAO0iYWVAStEvw/Hk4vO2Az6
Breaking Changes
Data Functions -> Preload Functions
We will replace the
data
prop on the router withpreload
. It will fire at the same time thatdata
did and when preload is triggered by other means but there is no return value. The router doesn't store the data, but the cache solution of your choice can.Return to lower case navigation elements
<a>
&<form>
Besides reducing the noise of imports these elements + delegation means they can work even in sections that aren't hydrated. Capital
<A>
will still exist (for the time being) with its ability to handle active class, but it is innately an Island/Client component and this way this works for all.Removal of
<Outlet>
,<Routes>
,useRoutes
<Outlet>
is no longer used and instead will useprops.children
passed from into the page components. This preseves that the chain is connected for future features like Server Components. Nested<Routes>
inherently cause waterfalls and are Outlets in a sense themselves. We do not want to encourage the pattern. Upon observation of solutions concerned with data fetching these are not common to see. The feature isn't available in Tanstack or Next App Router, and discouraged in use with Remix.These changes mean the
<Router>
directly receives the routes now (JSX or config) as children. Remember you can always add top level layout by wrapping your<Route>
s with another without a path.element
prop removed fromRoute
Related without
<Outlet>
component children need to be passed in manually. At which point theelement
prop has less value. Removing the second way to define route components to reduce confusion and edge cases.New Data APIs
I made a simple prototype without the router to test these APIs (it doesn't have the preload/cache life): https://codesandbox.io/s/route-cache-store-action-draft-dpf3f3?file=/src/index.tsx
cache
To prevent duplicate fetching and to trigger handle refetching we provide a cache api. That takes a function and returns the same function.
It is expected that the arguments to the cache function are serializable.
This cache accomplishes the following:
This cache can be defined anywhere and then used inside your components with:
createAsync
This is light wrapper over
createResource
. It is a simpler async primitive where the function tracks likecreateMemo
and it expects a promise back that it turns into a Signal. Reading it before it ready causes Suspense/Transitions to trigger.cache
API can be used withcreateResource
but it must be passed into the tracking argument not the fetcher which means that you can't only pass a single argument ascreateResource
defaults to just the fetcher. So to use a cache function withcreateResource
you would need to:action
Actions are data mutations that can trigger invalidations and further routing. A list of prebuilt response builders can be found below.
Notes of
<form>
implementation and SSRThis requires stable references as you can only serialize a string as an attribute, and across SSR they'd need to match. The solution is providing a unique name.
useAction
Instead of forms you can use actions directly by wrapping them in a
useAction
primitive. This is how we get the router context.The outside of a form context you can use custom data instead of formData, and these helpers preserve types.
useSubmission
/useSubmissions
Are used to injecting the optimistic updates while actions are in flight. They either return a single Submission(latest) or all that match with an optional filter function.
Response helpers
redirect(path, status || options)
Returns a redirect response with ability to optionally set headers, or indicate invalidations
Unresolved Questions
cache
keys/invalidation?The idea of providing a
name
and creating it ourselves is easy, but we could ask for a function that takes the same arguments as the async function and use that to generate the key on each execution.reload
revalidate without usingredirect
APISince
actions
don't have an URL and generally aren't the same as where you fetched from you typically have to redirect back to the same page to have it reload when dealing with server functions. It would be nice if we could find a way to avoid this that can work when JS is disabled (ie we can't just set custom headers from the client).Alternatives
From a technical standpoint there aren't much in the way of alternatives. I've spent the last year or so working through the options and finally feel confident to propose this. That being said how we roll it out might be up to debate. Perhaps we can introduce a new router instead of changing the existing. But realistically maintenance would be a concern here.
Adoption Strategy
This change would be a breaking one and it would come in a specific minor version of Solid Router (as it isn't 1.0 yet). The previous versions would be available in NPM but this would replace what we have currently, forcing migration over time across the ecosystem.
Beta Was this translation helpful? Give feedback.
All reactions