Skip to main content

Isograph runtime

note

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
note

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 a require call that imports the generated Query/HomePage/entrypoint.ts file.
  • The developer calls const HomePage = useResult(fragmentReference); (or calls <FragmentReader fragmentReference={fragmentReference} />. FragmentReader is just a wrapper around useResult). This will attempt to read the Query.HomePage resolver. This will suspend if there isn't enough data in the store to read all of the data required by the HomePage 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 for useLazyReference 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 and useQueryLoader. These have not been implemented. The @isograph/react-disposable-state library contains their building blocks.
    • useResult and FragmentReader take additional parameters and network request options, not documented here.
  • 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 nearest Suspense boundary).
  • On second render, the useResult call is re-evaluated. This time, there is enough data to read the fields required by HomePage, so the HomePage 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 or Avatar). 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.

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

warning

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

warning

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.