Isograph runtime
This document is intentionally short, because much of the runtime is incomplete and liable to change.
Initial setup
Currently, there are two things you must to do to use Isograph:
- set up a network function by calling
setNetwork
- call
subscribe(() => setState({}))
, in order to make some root component re-render whenever anything in the store changes. This is obviously bad for performance.
You should also see the quickstart guide for more one-time setup, e.g. changes to the babelrc.js
.
Big picture
In order to make a network request and read the results, the following occurs:
- the developer calls
const {fragmentReference} = useLazyReference(iso(`entrypoint Query.HomePage`));
. This will make the network request when that component renders.- The babel plugin changes the
iso
entrypoint call to arequire
call that imports the generatedQuery/HomePage/entrypoint.ts
file.
- The babel plugin changes the
- The developer calls
const HomePage = useResult(fragmentReference);
. This will attempt to read theQuery.HomePage
resolver. This may suspend. In particular, if there isn't enough data in the store to read all of the data required by theHomePage
resolver, the call toread
will suspend.- It is a good practice to pass the
fragmentReference
to a child component, which is wrapped in a<Suspense>
boundary. This isn't required foruseLazyReference
to work correctly, but it does eliminate some edge cases (namely, if the network response takes too long to come back), and does make refetching on error easier. - In the future, there will be other APIs, akin to Relay's
loadQuery
anduseQueryLoader
. These have not been implemented. The@isograph/react-disposable-state
library contains their building blocks.
- It is a good practice to pass the
- The network response completes, and the normalization AST (part of the
Query/HomePage/entrypoint.ts
file) is used to write the data to the global store. - The subscribe callback is triggered, causing the component tree to re-render.
- On second render, the
read(fragmentReference)
call is re-evaluated. This time, there is enough data to read the fields required byHomePage
, so theHomePage
resolver function is called. Assuming it is a react component (i.e. the resolver was declared with@component
), we can then render the component as follows:<HomePage {...additionaProps} />
. - When
<HomePage />
renders, it may itself have selected other components (e.g.Header
orAvatar
). The data for these was likely provided by initial network request, so they will not suspend, and the whole tree will render.- In the future, when Isograph supports
@defer
or@stream
, child resolvers may suspend at this point. If data in the Isograph store changes, child resolvers may also suspend.
- In the future, when Isograph supports
Store
The Isograph store is a global map from strong IDs or "relative IDs" to fields. It should not be global! But it is, for now.
For NextJS, we need to clear the store on every request, since it otherwise would be shared across requests (including for different users).
Fetching and entrypoints
Declaring an iso
entrypoint literal results in the creation of an entrypoint.ts
file. This contains three things:
- The query text (in the future, we will support persisted queries as well.)
- The normalization AST, which is the data structure used to write the network response into the store.
- A hard require of the reader artifact.
In the future, one should be able to generate entrypoints that only contain the query text. The normalization and reader ASTs are not always necessary initially.
Normalization
When the network response comes back, Isograph iterates the normalization AST and the network response in parallel to write data to the store.
The normalization AST contains information about all of the server fields that are present in the network response, i.e. it does not stop at resolver boundaries. No resolver is present in the normalization AST; it deals purely with server fields.
The network response cannot be written into the store without the help of a normalization AST, because which field is a strong ID field will eventually be configurable, etc. If arbitrary JSON scalars are acceptable parts of the network response (and they currently are, due to being allowed by GraphQL), one also needs a normalization AST to know to treat an arbitrary JSON scalar that looks like a valid "regular ol'" GraphQL response as a scalar.
In addition, when the Relay team adopted normalization ASTs, normalization time fell by 85%, because using a normalization AST means you can avoid introspecting the network response.
How normalization works
This section is especially liable to change.
If an object has a strong ID (for now, this means "if it has an ID field"), the object will be written to the store under that ID. e.g. { id: 123, name: "Jerry Garcia" }
will be written to the store as 123: { id: 123, name: "Jerry Garcia" }
.
If an object does not have a strong ID, the object's ID in the Isograph store will be generated based on a path to the nearest parent which has a strong ID. So, if we encounter { id: 123, "Jerry Garcia", guitar: { type: "Fender Stratocastor" }}
, this will be written to the store as: 123: { id: 123, name: "Jerry Garcia", guitar: { __link: "123.guitar" } }
and 123.guitar: { type: "Fender Stratocastor" }
.
Reading
This section is especially liable to change.
Right now, when a resolver is read, all server fields selected by that resolver are read. If the resolver selected any child resolvers (e.g. if full_name
is a resolver in User.address { full_name }
), then those are also read.
If a resolver is not defined with @component
, then if any selected server fields are missing or if any selected child resolvers return { kind: "MissingData" }
, then the resolver itself returns { kind: "MissingData" }
.
On the other hand, resolvers defined with @component
will never return { kind: "MissingData" }
when read out by parent resolvers. Instead, they only suspend when rendered. This allows you to strategically place suspense boundaries.
Changes to data in the store and subscribe
If any data changes in the store (for now, this can only occur through other network responses being received and normalized), the top-level subscribe
callback is called. Thus, the entire component tree re-renders.
In the future, reader ASTs can be used to isolate re-renders to just parts of the component tree that need to re-render.