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:
- create an environment, which important contains a network function that knows how to hit your GraphQL endpoint
- put that in an
IsographEnvironmentProvider
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);
(or calls<FragmentReader fragmentReference={fragmentReference} />
.FragmentReader
is just a wrapper arounduseResult
). This will attempt to read theQuery.HomePage
resolver. This will suspend if there isn't enough data in the store to read all of the data required by theHomePage
resolver, as is the case when the network request is initially in flight.- It is a best practice to pass the
fragmentReference
to a child component, which is wrapped in a<Suspense>
boundary, or to wrap the<FragmentReader />
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. useResult
andFragmentReader
take additional parameters and network request options, not documented here.
- It is a best practice to pass the
- The call
useResult
will suspend on the network request. - The network request completes, and the normalization AST (part of the
Query/HomePage/entrypoint.ts
file) is used to write the data to the global store. - This will cause React to retry rendering the
FragmentReader
(actually, everything underneath the nearestSuspense
boundary). - On second render, the
useResult
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 map IDs or "relative IDs" to records. It is contained in the Isograph environment. It will soon be a map from typename -> ID, in order to relax the requirement that IDs must be globally unique.
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.