Skip to content

enskit

enskit is the React toolkit for ENSv2 development. It provides a fully typed Omnigraph API client (powered by urql and gql.tada), the OmnigraphProvider, and the useOmnigraphQuery hook for writing type-safe ENS queries with editor autocomplete, Relay-style pagination, and Omnigraph-specific cache directives.

This guide walks you from an empty directory to a working React component that renders an ENS Domain and a paginated list of its subdomains — the same flow as the DomainView in our example app.

1. Scaffold a React app

If you already have a React + TypeScript app, skip ahead to Install enskit and enssdk.

Otherwise, the fastest way to get going is Vite:

Terminal window
npm create vite@latest my-ens-app -- --template react-ts
cd my-ens-app
npm install

2. Install enskit and enssdk

Terminal window
npm install enskit@1.13.1 enssdk@1.13.1

Pin exact versions

Always pin exact versions (no ^ or ~) of enskit and enssdk, and keep them on the same version. The Omnigraph GraphQL schema is bundled inside enssdk and consumed by the gql.tada TypeScript plugin to type your queries — a minor or patch bump can change the schema and silently drift your generated types away from your queries. Locking exact versions keeps types and runtime in sync, and matched to the ENSNode version your hosted instance runs.

3. Configure the gql.tada TypeScript plugin

gql.tada is what gives your graphql(...) query strings end-to-end type safety. It reads the Omnigraph schema from enssdk at typecheck time.

Add the plugin to tsconfig.json:

tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"plugins": [
{
"name": "gql.tada/ts-plugin",
"schema": "node_modules/enssdk/src/omnigraph/generated/schema.graphql",
"tadaOutputLocation": "./src/generated/graphql-env.d.ts"
}
]
},
"include": ["src"]
}

If you’re using VS Code, make sure your workspace is using the workspace TypeScript version so the plugin loads. Add this to .vscode/settings.json:

.vscode/settings.json
{
"js/ts.tsdk.path": "node_modules/typescript/lib",
"js/ts.tsdk.promptToUseWorkspaceVersion": true
}

4. Mount the OmnigraphProvider

OmnigraphProvider is what useOmnigraphQuery reads from. Construct an EnsNodeClient, extend it with the omnigraph module, and wrap your app:

src/App.tsx
import { OmnigraphProvider } from "enskit/react/omnigraph";
import { createEnsNodeClient } from "enssdk/core";
import { omnigraph } from "enssdk/omnigraph";
import { StrictMode } from "react";
import { DomainView } from "./DomainView";
// you may use a NameHash Hosted ENSNode instance
// learn more at https://ensnode.io/docs/hosted-instances
const ENSNODE_URL = import.meta.env.VITE_ENSNODE_URL!
// create and extend an EnsNodeClient with Omnigraph support
const client = createEnsNodeClient({ url: ENSNODE_URL }).extend(omnigraph);
export function App() {
return (
<StrictMode>
<OmnigraphProvider client={client}>
<h1>My ENS App</h1>
<DomainView />
</OmnigraphProvider>
</StrictMode>
);
}

5. Hello world

Create src/DomainView.tsx. We’ll start with the simplest possible query — look up the eth Domain and render its owner and protocol version.

InterpretedName

An InterpretedName is a Name whose labels are each either normalized or represented as an encoded labelhash (e.g. [abcd...].eth) — the canonical, lossless form ENSNode uses to identify a Name. asInterpretedName("eth") brands a known-safe string as one; for user input, validate first. See Interpreted Name in the terminology reference.

src/DomainView.tsx
import { graphql, useOmnigraphQuery } from "enskit/react/omnigraph";
import { asInterpretedName, beautifyInterpretedName } from "enssdk";
const DomainByNameQuery = graphql(`
query DomainByName($name: InterpretedName!) {
domain(by: { name: $name }) {
__typename
name
owner { address }
}
}
`);
export function DomainView() {
const name = asInterpretedName("eth");
const [result] = useOmnigraphQuery({
query: DomainByNameQuery,
variables: { name },
});
const { data, fetching, error } = result;
if (!data && fetching) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!data?.domain) return <p>No domain found.</p>;
const { domain } = data;
return (
<div>
<h2>
{domain.name
? beautifyInterpretedName(domain.name)
: "Unnamed Domain"}
</h2>
<p>Version: {domain.__typename}</p>
<p>
Owner: <code>{domain.owner?.address ?? "0x0"}</code>
</p>
</div>
);
}

A few things to notice:

  • graphql(...) parses your query at typecheck time. Hover over result.data and you’ll see it’s typed exactly to your selection set — try removing owner { address } from the query and watch the access below become a type error.
  • domain is a union of ENSv1Domain | ENSv2Domain (both implement the Domain interface). The Omnigraph unifies ENSv1 and ENSv2 behind the same query — __typename tells you which one you got.
  • name may be null for non-canonical names (e.g. Domains whose name cannot be inferred). Always guard the access; TypeScript will help you.

6. List subdomains

Expand the query to also fetch the Domain’s subdomains. subdomains is a Relay Connection, so the shape is { edges: [{ node }] }.

src/DomainView.tsx
const DomainByNameQuery = graphql(`
query DomainByName($name: InterpretedName!) {
domain(by: { name: $name }) {
__typename
name
owner { address }
subdomains {
edges {
node {
name
owner { address }
}
}
}
}
}
`);
export function DomainView() {
const name = asInterpretedName("eth");
const [result] = useOmnigraphQuery({
query: DomainByNameQuery,
variables: { name },
});
const { data, fetching, error } = result;
if (!data && fetching) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!data?.domain) return <p>No domain found.</p>;
const { domain } = data;
return (
<div>
<h2>{domain.name ? beautifyInterpretedName(domain.name) : "Unnamed Domain"}</h2>
<p>Version: {domain.__typename}</p>
<p>Owner: <code>{domain.owner?.address ?? "0x0"}</code></p>
<h3>Subdomains</h3>
<ul>
{domain.subdomains?.edges.map(({ node }, i) => (
<li key={i}>
{node.name
? beautifyInterpretedName(node.name)
: <em>unnamed</em>}{" "}
— Owner <code>{node.owner?.address ?? "0x0"}</code>
</li>
))}
</ul>
</div>
);
}

7. Extract a typed fragment

Notice we’re selecting the same fields (name, owner { address }) on the parent Domain and on each subdomain. Extract a DomainFragment to deduplicate the selection — and get a reusable, fully-typed shape for components that render a Domain.

src/DomainView.tsx
import {
type FragmentOf,
graphql,
readFragment,
useOmnigraphQuery,
} from "enskit/react/omnigraph";
import { asInterpretedName, beautifyInterpretedName } from "enssdk";
const DomainFragment = graphql(`
fragment DomainFragment on Domain {
__typename
name
owner { address }
}
`);
const DomainByNameQuery = graphql(
`
query DomainByName($name: InterpretedName!) {
domain(by: { name: $name }) {
...DomainFragment
subdomains {
edges { node { ...DomainFragment } }
}
}
}
`,
[DomainFragment],
);
function RenderDomain({ data }: { data: FragmentOf<typeof DomainFragment> }) {
// type-safe access to fragment data!
const domain = readFragment(DomainFragment, data);
return (
<>
<span>
{domain.name
? beautifyInterpretedName(domain.name)
: "Unnamed Domain"}
</span>{" "}
<span>({domain.__typename})</span>{" "}
<span>
— Owner <code>{domain.owner?.address ?? "0x0"}</code>
</span>
</>
);
}
export function DomainView() {
const name = asInterpretedName("eth");
const [result] = useOmnigraphQuery({
query: DomainByNameQuery,
variables: { name },
});
const { data, fetching, error } = result;
if (!data && fetching) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!data?.domain) return <p>No domain found.</p>;
return (
<div>
<h2><RenderDomain data={data.domain} /></h2>
<h3>Subdomains</h3>
<ul>
{data.domain.subdomains?.edges.map(({ node }, i) => (
<li key={i}>
<RenderDomain data={node} />
</li>
))}
</ul>
</div>
);
}

FragmentOf<typeof DomainFragment> is the opaque type for any selection that includes ...DomainFragmentRenderDomain accepts any of them. readFragment(DomainFragment, data) unwraps that opaque type to the typed fields you declared.

8. Paginate with “Load more”

subdomains is a Relay Connection — page through it with the first and after arguments. Add pageInfo { hasNextPage endCursor } to the query, track the cursor in component state, and wire up a “Next page” button.

src/DomainView.tsx
import { useState } from "react";
// ...other imports
const DomainByNameQuery = graphql(
`
query DomainByName($name: InterpretedName!, $first: Int!, $after: String) {
domain(by: { name: $name }) {
...DomainFragment
subdomains(first: $first, after: $after) {
edges { node { ...DomainFragment } }
pageInfo { hasNextPage endCursor }
}
}
}
`,
[DomainFragment],
);
const PAGE_SIZE = 20;
export function DomainView() {
const name = asInterpretedName("eth");
const [after, setAfter] = useState<string | null>(null);
const [result] = useOmnigraphQuery({
query: DomainByNameQuery,
variables: { name, first: PAGE_SIZE, after },
});
const { data, fetching, error } = result;
if (!data && fetching) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!data?.domain) return <p>No domain found.</p>;
const { subdomains } = data.domain;
return (
<div>
<h2><RenderDomain data={data.domain} /></h2>
<h3>Subdomains</h3>
<ul>
{subdomains?.edges.map(({ node }, i) => (
<li key={i}>
<RenderDomain data={node} />
</li>
))}
</ul>
{subdomains?.pageInfo.hasNextPage && (
<button
type="button"
disabled={fetching}
onClick={() => setAfter(subdomains.pageInfo.endCursor)}
>
{fetching ? "Loading..." : "Next page"}
</button>
)}
</div>
);
}

9. Run it

Terminal window
VITE_ENSNODE_URL=https://api.v2-sepolia.ensnode.io npm run dev

Open the printed URL and you should see the eth Domain, its owner, and the first page of its subdomains. Clicking Next page advances the cursor.

Where to go next

  • Try the Interactive Example: edit and run the full enskit-react-example app in your browser with a live preview.
  • Swap the hardcoded "eth" for a name from props or a router — see EnsureInterpretedName in the example app for safe handling of user-provided names.
  • See Omnigraph examples for ready-to-copy queries: account-owned domains, events, registrar permissions, full-text search, and more.
  • See the Omnigraph Schema Reference for the full set of types, fields, and arguments you can query.
  • Need data outside React? Use enssdk directly with the same graphql(...) helper.