Skip to content
IC Reactor

Next.js Integration

This example shows how to use IC Reactor in an existing Next.js app that still uses dfx for local deployment and declaration generation.

  • Next.js Pages Router: The app lives under src/pages, not the App Router.
  • Motoko Backend: A local todo canister under backend/.
  • Manual declaration flow: dfx generate output is committed under src/declarations/todo/.
  • IC Reactor hooks: createReactorProvider builds a defineReactor over those generated declarations once per render tree, and components read its typed hooks with useTodo().
Terminal window
cd examples/nextjs
npm run install:all
npm run dfx:start
npm run deploy
npm run generate
npm run dev

The snippets import with relative paths. The demo’s own files import the same modules bare (declarations/todo, service/provider), which its tsconfig.json resolves through paths: { "*": ["./src/*"] }.

src/service/provider.tsx
import { createReactorProvider, defineReactor } from "@ic-reactor/react"
import { canisterId, idlFactory } from "../declarations/todo"
import type { _SERVICE } from "../declarations/todo/todo.did"
export const { ReactorProvider: ICReactorProvider, useReactor: useTodo } =
createReactorProvider(() =>
defineReactor<_SERVICE>({
name: "todo",
canisterId,
idlFactory,
agentOptions: {
host: process.env.NEXT_PUBLIC_IC_HOST || "http://127.0.0.1:4943",
},
})
)

The factory runs once per mounted tree, and a server render is its own tree, so each request gets its own QueryClient, ClientManager, reactor and AuthenticationManager. Components take their hooks from useTodo(), which is typed as defineReactor’s result, so useActorQuery keeps its own signature: functionName is checked and data is typed without a wrapper or a cast.

src/components/Todos.tsx
import { useTodo } from "../service/provider"
const Todos = () => {
const { useActorQuery } = useTodo()
const { data, isPending } = useActorQuery({
functionName: "getAllTodos",
refetchInterval: 5000,
})
// data: Todo[] | undefined
}

Then wrap the app once, and keep anything that touches the reactor below it. The provider also renders a QueryClientProvider for the reactor’s QueryClient, which the React Query Devtools read:

src/pages/_app.tsx
import type { AppProps } from "next/app"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import { ICReactorProvider } from "../service/provider"
const App: React.FC<AppProps> = ({ Component, pageProps }) => (
<ICReactorProvider>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} position="bottom" />
</ICReactorProvider>
)

Nothing waits for clientManager.initialize() before rendering: on a local replica the agent fetches the root key before its first request, and useAuth() initializes the agent while it restores the session. The pages therefore render on the server as well, and the static export carries their HTML: the todo list in its loading state and the login button in the isAuthenticating state the auth hooks show until the session is read.