How to use Next.js on the frontend

Hello!

I am looking to use Next.js for my front end.
I have created a project using npx create and imported the files generated by dfx new (the backend is Rust).

I finally got dfx deploy to work, but when I connect to the front end, I get a “not found” message.

I hope someone can help me.

Here is the repository with all the source code.

Currently, we have not yet implemented the connection between the front end and the back end. First of all, I would like to be able to deploy the front end to the dfx local environment.

Next.js is producing static files. If you get a simple index.html working, then next.js should work too.
What happens is, all static files get uploaded to an asset canister.
The problem may be, that next.js is trying to figure out your page from the URL and there is nothing matching ?canisterid=ry....
It will work if you are using npm run dev without trying to locally deploy it. You can still connect it to the IC local backend, while the dev frontend gets served by a bundler, not local replica. (That’s what I am doing with my projects - for faster reloading when changing code)
Once you deploy it on IC in production, URLs will fix and it should work.

If you want to point a local domain to a canister, so your URLs work without ?canisterId, then you can use icproxy. Something like this

icx-proxy --address 127.0.0.1:8453 --dns-alias 0.lvh.me:rno2w-sqaaa-aaaaa-aaacq-cai

anything.lvh.me will point to 127.0.0.1 and you can change ‘anything’

Thanks for your response.
Indeed, npm run dev gives me access to the front end.

Now, when I try to call the backend code, I get the following error Perhaps there is something wrong with the way the environment variables are set…

error - Error: Must specify a host to connect to.
    at new HttpAgent 
    at createActor (webpack-internal:///./src/declarations/backend/index.js:18:36)
    at eval (webpack-internal:///./src/declarations/backend/index.js:38:17)
    at Object../src/declarations/backend/index.js 
    at __webpack_require__ 
    at eval (webpack-internal:///./src/service/actor.ts:6:79)
    at Object../src/service/actor.ts 
    at __webpack_require__ 
    at eval (webpack-internal:///./src/container/Greeting.tsx:7:71)
    at Object../src/container/Greeting.tsx  {
  page: '/'
}

I understand that this is implemented using Next.js, but if you have a repository, etc., I would love to see it. I’d love to use it as a reference for my build :slightly_smiling_face:!

The error shows because the frontend can’t connect to the backend. I havent used this repo nor rust. If it was me, I would start with clean nextjs and then just add one dfx.json with the frontend can. Then dfx deploy will work.
Everything backend, I would prefer to write in Motoko.

I guess the real reason one would prefer Nextjs over Create-react-app is because it generates static HTML files that search engines can crawl. This starter kit is not demonstrating that.
So the question is, what do you really want to do. Are you planning on relying on indexable website or do you want to create an application? If it’s the second, just use React CRA.

1 Like

Axon is a next.js app…it might have some clues. GitHub/floorlamp/axon I think.

1 Like

The error most probably happens when Next.js prerender your app - i.e. this error find place because the prerendering of your app happens in a NodeJS context (on the server side) and not in a browser context where agent-js uses the window object to determine the host to connect to.

If you do not want or need to prerender the data provided by the IC to build your app, you can either lazy load the service that loads the data only on the client side or dynamically import the components that access the IC to skip the fetching of the data.

In both case, the tricks is to not import the part that should not be prerendered.

Lazy load service:

// Pseudo code snippet
const isBrowser = () => typeof window !== `undefined`;

if (isBrowser) {
  const {myFunc} = await import("/path/to/service");
  myFunc()
}

Next dynamic import with ssr: false (more in doc https://nextjs.org/docs/advanced-features/dynamic-import)

import dynamic from 'next/dynamic'

const DynamicHeader = dynamic(() => import('../components/header'), {
  ssr: false,
})
2 Likes

My understanding of the part about using static files was unclear: I specified the file generated by running next export during the next build in dfx.json, set the entry point, and was able to access it from local. Thank you!

Thanks for sharing! I will refer to it.

Thanks for the feedback!

I just used dynamic and now I can call the function!

[index.tsx]

import dynamic from 'next/dynamic';
import styles from '../styles/Home.module.css';

const WhoamiButton = dynamic(() => import( '../components/WhoamiButton'),{ssr:false})

export default function Home() {
  return (
    <div>
      <main className={styles.main}>
        <h1 className="text-red-500">
          <WhoamiButton></WhoamiButton>
        </h1>
      </main>
    </div>
  );
}

[WhoamiButton.tsx]

import Button from './Button'
import {backend} from '../../declarations/backend'

function whoami() {
  const whoami = backend.whoami()

  alert(`whoami: ${whoami}`)

  console.log(`whoami: ${whoami}`)
}

const WhoamiButton = () => {
  return (
    <Button onClick={whoami}>whoami</Button>
  )
}

export default WhoamiButton

I have one question here.

When I connected to the local replica, there was no error message,

but with the next env(localhost:3000), I get an error message after calling the function (pushing the button).

Do you know why this error occurs :thinking:?

hard to tell but I guess the proxy that should route the calls to the local replica (localhost:3000 → 127.0.0.1:4943) is not set. never tried Next.js with agent my self, so really not sure but I would search in that direction if I had to debug

side node: note the issue but whoami returns a promise

function async whoami() {
  const whoami = await backend.whoami()

  ...
1 Like

It may be better if you just edit the declarations/…did.js and remove the export at the very bottom


You will now have to create your actor when you need it, so there won’t be a need to dynamically import.

2 Likes

Cool to know I am not the only one removing it @infu! Have you upvoted https://github.com/dfinity/sdk/discussions/2761 ?

For those interested, meanwhile, I’ve got an ugly script that I run after generation of the types to perform such clean up. If it can be useful to some, there you go:

const cleanTypes = async ({ dest = `./src/declarations` }) => {
	const promises = readdirSync(dest).map(
		(dir) =>
			new Promise(async (resolve) => {
				const indexPath = join(dest, dir, 'index.js');

				if (!existsSync(indexPath)) {
					resolve();
					return;
				}

				const content = await readFile(indexPath, 'utf-8');
				const clean = content
					.replace(/export const \w* = createActor\(canisterId\);/g, '')
					.replace(/export const canisterId = process\.env\.\w*_CANISTER_ID;/g, '');

				await writeFile(indexPath, clean, 'utf-8');

				resolve();
			})
	);

	await Promise.all(promises);
};

(async () => {
	try {
		await cleanTypes({});

		console.log(`Types declarations processed!`);
	} catch (err) {
		console.error(`Error while processing the types declarations.`, err);
	}
})();

Note: above script also removes canisterId because in my dapps it leads to conflicts

1 Like

Yeah. I actually leave the original files alone and create& use another file outside ‘declarations’ where I am changing a lot of things.
Instead of all being named and exported as ‘createActor’ and later imported and renamed, I am just naming them something like createSomethingActor

1 Like

Same here :smiley::+1:

e.g.

import {Actor, ActorMethod, ActorSubclass, HttpAgent, Identity} from '@dfinity/agent';
import {IDL} from '@dfinity/candid';
import {Principal} from '@dfinity/principal';
import {EnvStore} from '../stores/env.store';

export const createActor = async <T = Record<string, ActorMethod>>({
  canisterId,
  idlFactory,
  identity
}: {
  canisterId: string | Principal;
  idlFactory: IDL.InterfaceFactory;
  identity: Identity;
}): Promise<ActorSubclass<T>> => {
  const host: string = EnvStore.getInstance().localIdentity()
    ? 'http://localhost:8000/'
    : 'https://ic0.app';

  const agent: HttpAgent = new HttpAgent({identity, ...(host && {host})});

  if (EnvStore.getInstance().localIdentity()) {
    // Fetch root key for certificate validation during development
    await agent.fetchRootKey();
  }

  // Creates an actor with using the candid interface and the HttpAgent
  return Actor.createActor(idlFactory, {
    agent,
    canisterId
  });
};

Merry Xmas :christmas_tree:

1 Like