Skip to main content

Quickstart guide

In this quickstart guide, we will create a new NextJS project and add Isograph to it. Then we'll use the free and publicly available Star Wars GraphQL API.

You can view the end result of following this quickstart guide in this repository.

tip

If you want an example using Vite rather than NextJS, check out the Vite Pokemon demo to review the configuration needed for Vite.

Install NextJS

In a newly-created empty directory, run:

npx create-next-app@latest . \
--ts --eslint --no-app --src-dir \
--no-tailwind --import-alias "@/*"

This will install a NextJS app in this folder. Run it with npm run dev.

Install the compiler, Babel plugin and runtime

yarn add --dev @isograph/compiler
yarn add --dev @isograph/babel-plugin
yarn add @isograph/react

Installing the compiler also adds the command yarn iso and yarn iso --watch. But hang tight — before this command works, you'll need to create a folder, download your schema and create an isograph.config.json file!

Create an isograph.config.json

Create an isograph.config.json file. You can use the following for this quickstart:

{
"project_root": "./src/components",
"artifact_directory": "./src/components",
"schema": "./schema.graphql"
}
note

The artifact_directory field is optional, and defaults to the project_root. Feel free to skip it.

Add aliases to tsconfig.json

Add two aliases to your tsconfig.json's compilerOptions field. These alias should point to artifact_directory, followed by __isograph/* and __isograph/iso.ts. Here is a snippet of a tsconfig showing the paths field, correctly set up for this quickstart:

{
"compilerOptions": {
"paths": {
"@iso": ["./src/components/__isograph/iso.ts"]
}
}
}

Disable React strict mode

NextJS defaults to using strict mode. Isograph is currently incompatible with strict mode. Disable strict mode in your next.config.js file as follows:

// next.config.js
const nextConfig = {
reactStrictMode: false,
};
Why is this necessary?

See this FAQ item for an explanation.

Create a .babelrc.js

To enable Babel and the Isograph Babel plugin, create a .babelrc.js with the following contents:

module.exports = {
presets: ['next/babel'],
plugins: ['@isograph'],
};
What about SWC?

Isograph currently requires a Babel plugin, but there is an open, good first issue to make it work with SWC.

Download the schema

Download your GraphQL schema and put it in ./schema.graphql:

curl https://raw.githubusercontent.com/graphql/swapi-graphql/master/schema.graphql > ./schema.graphql

Run the compiler in watch mode

yarn iso --watch

The compiler will start running, but since we haven't written any Isograph literals, it won't do much.

note

The Isograph compiler can be a bit finicky, especially if you're still learning the syntax. If the process stops, don't panic — just fix the error and restart the compiler.

Teach Isograph how to make network requests

Isograph requires some initial setup to teach it how to make API calls to your GraphQL server. The GraphQL server we will hit is running at https://swapi-graphql.netlify.app/.netlify/functions/index.

In our case, we can do that by changing our src/pages/_app.tsx file to look like:

import { useMemo, Suspense } from 'react';
import type { AppProps } from 'next/app';
import {
createIsographEnvironment,
createIsographStore,
IsographEnvironmentProvider,
} from '@isograph/react';

function makeNetworkRequest<T>(
queryText: string,
variables: unknown,
): Promise<T> {
const promise = fetch(
'https://swapi-graphql.netlify.app/.netlify/functions/index',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: queryText, variables }),
},
).then(async (response) => {
const json = await response.json();

if (response.ok) {
return json;
} else {
throw new Error('NetworkError', {
cause: json,
});
}
});
return promise;
}

export default function App({ Component, pageProps }: AppProps) {
const environment = useMemo(
() =>
createIsographEnvironment(
createIsographStore(),
makeNetworkRequest,
// Optional missing field handler
null,
// Optional logger
console.log,
),
[],
);
return (
<IsographEnvironmentProvider environment={environment}>
<Suspense fallback="loading">
<Component {...pageProps} />
</Suspense>
</IsographEnvironmentProvider>
);
}

In this step, we created some context that holds the Isograph environment. The Isograph environment contains the data that we have received from the network and knows how to make network requests to the GraphQL backend.

Why are we wrapping Component in a suspense boundary?

We're wrapping the inner <Component /> in a suspense boundary because later, we'll render <HomePage />. This component will suspend if data is missing. When the network request completes and the component unsuspends, React will re-render the children of the suspense boundary.

When React re-renders the children of a suspense boundary, their hooks lose all state. We memoize the Isograph environment out side of the suspense boundary because, if the environment were defined within the suspense boundary, it would be recreated in an infinite loop :'(

Create the Root.HomePage component

Finally, we can get to building our first client field, the Root.HomePage component!

note

In the Star Wars API, the query object is Root (i.e. the Star Wars schema contains schema { query: Root }). That's why in this quickstart, we define a client field on the Root object: Root.HomePage.

If you're following along with this quickstart, but using a different schema, then you most likely will want to define a client field on a different object, such as Query.HomePage!

An Isograph app will be almost entirely made up of client fields. There are two important important facts about client fields that you should know:

  • they can reference each other. In this quickstart, Root.HomePage will reference Film.FilmSummary.
  • they can return arbitrary values. In this quickstart, both fields will return React elements. A field that return a React elements is called a client component field.

So, let's define our first field, Root.HomePage, by making sure that yarn iso --watch is running and then creating a file (e.g. src/components/HomePage.tsx) containing the following:

import React from 'react';
import { iso } from '@iso';

export const HomePage = iso(`
field Root.HomePage @component {}
`)(function HomePageComponent({ data }) {
return 'Hello from the home page!';
});

That's it! That's our first Isograph component. Let's break down what we just did.

  • We defined a field named HomePage on the type Root, which our GraphQL schema has defined as our query "root operation type".
  • We wrote @component to tell the Isograph compiler that this field is a component.
  • Then, we passed a simple React component to this iso literal.

Let's proceed by selecting some fields. Modify the export as follows:

export const HomePage = iso(`
field Root.HomePage @component {
allFilms {
films {
id
title
episodeID
}
}
}
`)(function HomePageComponent({ data }) {
return 'Hello from the home page!';
});

Now, when the component is called, the first argument (data) will have type:

type Data = {
allFilms: {
films: ({
id: string;
title: string | null;
episodeID: number | null;
} | null)[];
} | null;
};

Every time you save, the Isograph compiler will recompile everything, including re-generating the type of the iso function. This means that TypeScript knows the type of the data parameter without you having to do anything!

Let's complete this component by returning a list of the films, their titles and episode names. The entire file should now look like:

import React, { useMemo } from 'react';
import { iso } from '@iso';

function nonNullable<T>(value: T): value is NonNullable<T> {
return value != null;
}

function toSorted<T>(arr: T[], comparator: (a: T, b: T) => number): T[] {
const sorted = [...arr];
sorted.sort(comparator);
return sorted;
}

export const HomePage = iso(`
field Root.HomePage @component {
allFilms {
films {
id
title
episodeID
}
}
}
`)(function HomePageComponent({ data }) {
const films = useMemo(
() =>
toSorted(data.allFilms?.films ?? [], (film1, film2) => {
if (film1?.episodeID == null || film2?.episodeID == null) {
throw new Error(
'This API should not return null films or null episode IDs.',
);
}
return film1.episodeID > film2.episodeID ? 1 : -1;
}).filter(nonNullable),
[data.allFilms?.films],
);

return (
<>
<h1>Star Wars Film Archive</h1>
{films.map((film) => (
<h2 key={film.id}>
Episode {film.episodeID}: {film.title}
</h2>
))}
</>
);
});

Make a network request for the data that the Root.HomePage component needs

That Isograph component isn't doing much on its own. We need to fetch the server fields it requested.

In order to fetch the data, Isograph requires that you define an entrypoint. An entrypoint definition might look like iso(`entrypoint Root.HomePage`). When the Isograph compiler encounters an entrypoint definition, it generates a GraphQL query for all of the fields reachable from that field:

query HomePage {
allFilms {
films {
id
episodeID
title
}
}
}

So, create a file at src/components/HomePageRoute.tsx, and make its contents:

import React from 'react';
import { useLazyReference } from '@isograph/react';
import { iso } from '@iso';

export default function HomePageRoute() {
const { fragmentReference } = useLazyReference(
iso(`entrypoint Root.HomePage`),
{
/* query variables */
},
);
return null;
}

and change src/pages/index.tsx to be:

import HomePageRoute from '@/components/HomePageRoute';

export default function Home() {
return <HomePageRoute />;
}

The useLazyReference function will make a network request when it is first rendered.

So, whenever we render the HomePageRoute component, the Isograph runtime will make a network request for all the fields selected by the Root.HomePage component. Try it! If you navigate to localhost:3000 and open the network tab, you'll see a network request that returns the fields we requested!

Render the component

Now, we still need to render our Query.HomePage component. In order to do this, we call useResult to read the query reference. This gives us the value of that field (i.e. a component), which we can render.

import React from 'react';
import { useLazyReference, useResult } from '@isograph/react';
import { iso } from '@iso';

export default function HomePageRoute() {
const { fragmentReference } = useLazyReference(
iso(`entrypoint Root.HomePage`),
{
/* query variables */
},
);
const HomePage = useResult(fragmentReference);
return <HomePage />;
}

Nice! Look at that beautiful list of Star Wars episodes!

Add a subcomponent

A key principle of React is that you can divide your components into subcomponents. Let's do that! For example, create src/components/EpisodeTitle.tsx containing:

import React from 'react';
import { iso } from '@iso';

export const EpisodeTitle = iso(`
field Film.EpisodeTitle @component {
title
episodeID
}
`)(function EpisodeTitleComponent({ data }) {
return (
<h2>
Episode {data.episodeID}: {data.title}
</h2>
);
});

Let's use this component by modifying HomePage.tsx to be the following. Note the two new sections:

import React, { useMemo } from 'react';
import { iso } from '@iso';

function nonNullable<T>(value: T): value is NonNullable<T> {
return value != null;
}

function toSorted<T>(arr: T[], comparator: (a: T, b: T) => number): T[] {
const sorted = [...arr];
sorted.sort(comparator);
return sorted;
}

export const HomePage = iso(`
field Root.HomePage @component {
allFilms {
films {
id
episodeID
EpisodeTitle
}
}
}
`)(function HomePageComponent({ data }) {
const films = useMemo(
() =>
toSorted(data.allFilms?.films ?? [], (film1, film2) => {
if (film1?.episodeID == null || film2?.episodeID == null) {
throw new Error(
'This API should not return null films or null episode IDs.',
);
}
return film1.episodeID > film2.episodeID ? 1 : -1;
}).filter(nonNullable),
[data.allFilms?.films],
);

return (
<>
<h1>Star Wars Film Archive</h1>
{films.map((film) => (
<film.EpisodeTitle key={film.id} />
))}
</>
);
});

Now, if you refresh, the UI will be divided into subcomponents and look exactly the same! Nice! 🎉

Congratulations

Congratulations! You just built your first Isograph app.

Want more? Try extracting the sorted list of films into its own client field (no need to use @component for this one.) Use hooks in your components (they work!) Check out the mutation, pagination, and the loadable fields documentation!

Or, join the Discord!