-
-
Notifications
You must be signed in to change notification settings - Fork 453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Fragment Suspense boundaries in React bindings #1408
Comments
Will it be possible to use Having cache updates only trigger re-renders in components that consume the affected data via a fragment vs triggering a re-render via the |
It isn't really what we were envisioning here to be completely honest. GraphQL is pretty great because it fits the mental model of componentised apps really well. Ultimately it's a way of Modeling relational data as it flows down from top to bottom in an app. Fragments enhance this by defining data requirements at every step along the way, which is why they're so great when used at each component that passes on and splits up data. What I've seen a lot is that in apps these responsibilities become easily conflated and these boundaries are ignored. But ultimately what I see work best is a differentiation between three types of components: "stateful components" (smart components), "structural components" (dumb components), and "presentational components" (often leaf components) So often times, if a team was really careful about this, you'd have data requirements composed to some kind of "top level" component (a page, a modal, an async/split component) that executes the query; it then starts passing on that data to structural components. A structural components only responsibility is to define data requirements and map those to presentational components and smart components. Therefore follows that any other component should not disturb this flow. Stateful components can add on state and interactivity (or other data) along this flow and presentational components should mostly be concerned about UI and hence none of the other 'types' of components should be rendered by presentational components, if possible. This fits really nicely into React's (and Vue's) Suspense vision. Consequentially Suspense is a mechanism for a structural component to render placeholders or loading elements when their data requirements aren't fulfilled. How does that fit into this discussion? It's possible for state containers to circumvent this structure. The simplest form of this and equivalent are providers in React; and often those are used for both data and state, which can lead to issues. So while a stateful container is as low in the tree as possible, libraries like "Valtio" and state containers are useful to keep stateful components low in the tree while still sharing updates and state changes across the app. I have yet to see an app where "teleporting" data is a better choice than preventing excessive rerenders at a structural component boundary 😅 This is because as you get deeper into an app you exponentially encounter more work; however, interrupting this work and avoiding it at times can be cheaper than teleporting data. Now, for these fragment containers to work a normalised cache needs deep integration with the bindings and client. And that's something that goes against our philosophy and principles that keep the API and internals simple and flexible. But the other problem is that "teleporting data" can still be understood as a layered problem. Ultimately, to recognise the fragment of data it needs two pieces of information: A keyable entity and a GraphQL fragment. So if you wanted this to also "teleport data" then you'd also need the keyable entity. So in other words, if we have However, if we also think about "teleporting this data" and not use suspense then we may as well just send the entire fragment via context or something like a state container. Which is nice but not an interesting problem. All it would achieve is "skip" over some structural components which increases complexity at the top level component but only ultimately skips less than a handful of structural components. So why suspense and this API? Given that a preferable element structure passes GraphQL data on until it hits more and more presentational parts of our app this also coincides with loading boundaries and is exactly what Suspense will be good at; allowing React to prepare parts of the tree until all data requirements are fulfilled. And Relay's mechanism isn't that different at that; it gets a handle for the fragment and checks for updates to enforce that the input data is actually updated when it needs to. My theory is that that's actually not necessary unless the suspense boundary that's triggered is above the query. But we likely can introduce a pretty simple mechanism to track what the fragment is accessing by exploiting that the fragment will be "seen" in the Anyway, sorry for the long write up. But I wanted to clarify how this all fits together and how this actually closely aligns with what the React team intended Suspense to be used for and what Relay wants these patterns to look like ✌️ because when I see "performance optimisation" in this context, I believe it's easy to lose track of how things are actually used ergonomically by devs 😅💙 |
An easy example for a frequent app update would be updates of x/y coordinates of a specific element deep down in the tree. Triggering a re-render of the whole component tree just for updating the position is perseived as an overkill to me. If the coordinates are instead injected via fragment only the one component that consumes the data deep in the tree must be updated. The same also could apply for table rows and columns, a chat feed etc. There are many use-cases where teleporting can be handy. |
@kitten Any updates here? I have the same query running on multiple pages with different subsets of fragments. The issue is that when I navigate pages the second page's query does not suspend but provides the components with partial results from the cache while it queries the rest. This results in errors as data that should not be null is not available.
Wondering if there is something I am missing. Should I be able to make a query suspend until it has fully fulfilled all its fragments? |
To be frank, that sounds more like you're relying on Graphcache's Schema Awareness mode?
That shouldn't ever be the case tbh 😅 However, the solution here is that we're waiting for GraphQL's nullability operators, to be exact. We're still playing around with alternatives (i.e. directives, which is Relay's approach) for those, but the intended behaviour is that you should be able to selectively specify which data a page/component needs and which it can lack while loading/displaying. Re. fragmented loading: that's a more tricky problem. In general, even with Schema Awareness or the above, you have to make a decision. You could say that some components should display a loading state connected to In the future, we'd obviously like to have something like One of the requirements is that this must however work with all bindings, i.e. it wouldn't be React-specific, although React/Vue/etc may have a However, that said, currently some of this is blocked because we're looking to provide TypeScript type-gen tooling of our own first, to enforce certain framework patterns that will make this easier. |
Thanks for the quick reply and insight!
I spoke too soon here, looks like this is an issue in my own implementation with some assertions on nullable data.
This is what I would be looking for, looking forward to see the progress here, thanks! |
Do you guys are using Other than that, cannot wait for this feature to land one day. There are too many people that are lacking understanding of how you should build a query (e.g. from the bottom of the tree upwards, not from top to bottom). What are "component data requirements", and how do fragments play a role here. Having |
I think they will use GraphQLSP but it is only a supposition. |
I have been thinking an increasing amount about this issue lately and started thinking that maybe there is a middle ground RE maybe not needing Suspense but still encouraging a fragment co-location story. Currently when a query TodosView {
todos(first: 10) {
id
text
completed
author {
...TodoFields_Author
}
}
}
fragment TodoFields_Author on Author {
id
name
} we have a component where Why does this matter? Currently it feels a bit like a pitfall that we can have the following document with i.e. co-located fragments. query ProductsView {
category(id: $id) {
id
name
filterOptions { id name values }
products(take: $limit, cursor: $cursor) {
id
name
}
}
} if we want to only be notified about Concretely in |
Building on the above, I see a few iterations here, we could in a minor release start supporting In a major release we could establish the separate subscriptions and work towards the idea of Note that nowadays we have three first-party clients that support |
This is due to the whole token being passed as a prop (causing a re-render) not just the required fragment fields. This should hopefully be resolved with urql-graphql/urql#1408
Summary
Overall, our new Suspense implementation is working a lot better, as expected, after getting some feedback on it from the React team. This gives us the room to experiment with more uses of Suspense in our React bindings.
Today when we load a query with Graphcache we may run into cases where Schema Awareness kicks in, when it's enabled.
Today this already allows people to have a page-wide query that loads some data and displays partial data when the cache doesn't have enough information to fulfil the query in its entirety but so far that it can leave out optional fields.
This poses a challenge for implementations, where some sub-components of a page may have to switch between a partial and a non-partial state. Assuming that these have types in a TypeScript environment, dealing with partial data on its own isn't too difficult. However, implementing many cascading loading states can be a difficult task and Suspense can help here.
Proposed Solution
It should be possible for a component to check data for completion using a
useFragment
suspense hook. This hook accepts a fragment and a piece of data and suspends when the fragment isn't fully fulfiled and partial.TBD: Exact API details pending #1221
Passive Update solution:
One solution that may work is to passively ingest React updates and
Client
results; i.e. theuseFragment
hook receives a fragment and data but will be used with auseQuery
hook that also triggers suspense updates, hence we'll eventually pre-render up until theuseFragment
hook(s). This may either have a separate suspense boundary or use a suspense boundary aboveuseQuery
. The important detail here is whether it has its own boundary.When a
useFragment
hook has its own boundary it'll likely have to look at all active queries and filter them down to ones that also consist the same fragment (by name). Hence, a code generation tool would be of prefereable use here. Once it detects that the same fragment occurs in anOperationResult
it should update and either throw a suspense promise or return a result as needed. The result should be masked as per the fragment.Requirements
useFragment
hook should accept data and a fragment and return masked datauseFragment
hook should suspend when its data is incompleteThe text was updated successfully, but these errors were encountered: