# enskit (React)

`enskit` is the React toolkit for ENSv2 development. It provides a fully typed Omnigraph API client (powered by [`urql`](https://nearform.com/open-source/urql/) and [`gql.tada`](https://gql-tada.0no.co/)), 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](/docs/concepts/the-ens-protocol) and a paginated list of its subdomains — the same flow as the [`DomainView`](https://github.com/namehash/ensnode/blob/main/examples/enskit-react-example/src/DomainView.tsx) in our example app.

<IntegrateHostedEnsNodeTip />

## 1. Scaffold a React app

If you already have a React + TypeScript app, skip ahead to [Install `enskit` and `enssdk`](#2-install-enskit-and-enssdk).

Otherwise, the fastest way to get going is [Vite](https://vite.dev):

```sh
npm create vite@latest my-ens-app -- --template react-ts
cd my-ens-app
npm install
```

## 2. Install enskit and enssdk

```
`npm install enskit@${snapshot.sdkVersion
```
**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`:

```json title="tsconfig.json" ins={6-12}
{
  "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`:

```json title=".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:

```tsx title="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 ?? "https://api.v2-sepolia.ensnode.io";

// 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](/docs/reference/terminology#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](/docs/reference/terminology#interpreted-name) in the terminology reference.

```tsx title="src/DomainView.tsx"
import { graphql, useOmnigraphQuery } from "enskit/react/omnigraph";
import { asInterpretedName } from "enssdk";

const DomainByNameQuery = graphql(`
  query DomainByName($name: InterpretedName!) {
    domain(by: { name: $name }) {
      __typename
      canonical { name { beautified } }
      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.canonical ? domain.canonical.name.beautified : "Unnamed Domain"}</h2>
      <p>Version: {domain.__typename}</p>
      <p>
        Owner: <code>{domain.owner?.address ?? "0x0"}</code>
      </p>
    </div>
  );
}
```
**BeautifiedName:** `beautified` is the display-ready form of the Canonical Name — its normalized labels rendered per [ENSIP-15](https://docs.ens.domains/ensip/15) (e.g. `♾.eth` → `♾️.eth`) — so you can render it directly with no normalization or emoji logic of your own. It's **display-only**: use `interpreted` (or the Domain `id`) as a lookup key or navigation target, never `beautified`. See [Beautified Name](/docs/reference/terminology#beautified-name).

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.
- `canonical` 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](https://relay.dev/graphql/connections.htm), so the shape is `{ edges: [{ node }] }`.

```tsx title="src/DomainView.tsx" ins={7-11,40-48}
const DomainByNameQuery = graphql(`
  query DomainByName($name: InterpretedName!) {
    domain(by: { name: $name }) {
      __typename
      canonical { name { beautified } }
      owner { address }
      subdomains {
        edges {
          node { canonical { name { beautified } } 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.canonical ? domain.canonical.name.beautified : "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.canonical ? node.canonical.name.beautified : <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 (`canonical { name { beautified } }`, `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.

```tsx title="src/DomainView.tsx" ins={1,4-10,16,18,23,26-39,57-59,65}
import { type FragmentOf, graphql, readFragment, useOmnigraphQuery } from "enskit/react/omnigraph";
import { asInterpretedName } from "enssdk";

const DomainFragment = graphql(`
  fragment DomainFragment on Domain {
    __typename
    canonical { name { beautified } }
    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.canonical ? domain.canonical.name.beautified : "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 `...DomainFragment` — `RenderDomain` 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.

```tsx title="src/DomainView.tsx" ins={1,6,9,11,19,23,27,53-61}
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

```sh
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](/docs/integrate/integration-options/enskit/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`](https://github.com/namehash/ensnode/blob/main/examples/enskit-react-example/src/DomainView.tsx) in the example app for safe handling of user-provided names.
- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy queries: account-owned domains, events, registrar permissions, full-text search, and more.
- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query.
- Need data outside React? Use [`enssdk`](/docs/integrate/integration-options/enssdk) directly with the same `graphql(...)` helper.

[Interactive Example ⚡](/docs/integrate/integration-options/enskit/example)

[enskit-react-example (GitHub)](https://github.com/namehash/ensnode/tree/main/examples/enskit-react-example)

[gql.tada documentation](https://gql-tada.0no.co/)

[urql documentation](https://nearform.com/open-source/urql/)