Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render icons on server side in NextJS 13 #234

Open
AestasLonewolf opened this issue Jul 11, 2023 · 16 comments
Open

Render icons on server side in NextJS 13 #234

AestasLonewolf opened this issue Jul 11, 2023 · 16 comments

Comments

@AestasLonewolf
Copy link

AestasLonewolf commented Jul 11, 2023

I'm trying to bundle icons when building instead of loading them on demand on client side.
The iconify documentation mentions "providing icon data as parameter instead of icon name"

I've tried the following

import { Icon } from '@iconify-icon/react'
import discordIcon from '@iconify/icons-logos/discord-icon'

// .....
 <Icon icon={discordIcon} width={25} id="discord-icon" />

but this still returns an empty <span></span> which is then filled on the client side.

Is it possible to server-side render these icons with NextJS' app dir?

@cyberalien
Copy link
Member

It is intended behaviour. Component renders span before it is mounted to avoid breaking hydration. If component renders different content on server and client sides, it would result in React failing to hydrate content and throw an error.

Rendering identical content on server and client sides is not always possible. Many icons use unique ids for masks, clip path, animations, reusable elements. IDs are supposed to be unique, so to avoid errors, component randomises those IDs for each render. This guarantees that two icons do not have same IDs. This also means that content is likely to be different on each render. To avoid breaking stuff, component renders span before it is actually mounted.

I recommend using Unplugin Icons for that instead of Iconify components: https://github.com/antfu/unplugin-icons

Unplugin Icons are designed to do exactly what you need: generate components for icons that are bundled, no extra overhead.

@mattrossman
Copy link

If component renders different content on server and client sides, it would result in React failing to hydrate content and throw an error.

@cyberalien Couldn't this be solved with React Server Components? OP mentioned using Next.js' app router, it seems like there could be a RSC-friendly version of the Icon component that doesn't require client rendering logic.

@cyberalien
Copy link
Member

Probably. However, it is a new thing and compatibility is a big issue. It needs to work in recent versions of React and all frameworks based on React, not just Next.js.

@cyberalien
Copy link
Member

After thinking more about it, I think it is time to rewrite component, targeting only latest React. Devs using older version can use older version of icon component.

@mattrossman
Copy link

Good idea.

I suppose solving the hydration problem is still necessary for folks who want to use it in Client Components and/or other frameworks with latest React.

I believe useId() is the idiomatic way of creating unique, stable IDs for elements in React 18 (docs). This PR suggests that it is stable across server & client for hydration in Next.js: vercel/next.js#31102. I would imagine other frameworks behave similarly.

@cyberalien
Copy link
Member

Revisited this issue and just noticed that in code sample in first post it uses @iconify-icon/react.

So I'm very confused. Issue mentions behaviour that applies to @iconify/react, but uses @iconify-icon/react in code sample.

@iconify-icon/react doesn't behave like I've described above. It is a simple wrapper for web component. It cannot and does not render span element.

@mattrossman
Copy link

Perhaps it was a mistake in their example. On my end when using the latest Next.js with app router:

  1. @iconify/react throws an error linking here when I use the Icon component as-is, so I can't inspect the SSR output. Converting this to a client component by re-exporting from a new file with a "use-client" directive allows it to hydrate and display on the page. The SSR output in this case is an empty <span></span>
"use client";

export { Icon } from "@iconify/react";
  1. @iconify-icon/react produces a SSR output of an <iconify-icon> element, which displays nothing on the page unless I turn it into a client component the same way.
"use client";

export { Icon } from "@iconify-icon/react";

In both cases, using the Icon component as a client-component causes it to visibly "pop-in" and after the page hydrates. My desired behavior would be to use the Icon component without wrapping in "use-client" and have the SVG content be included in the pre-rendered HTML.

@cyberalien
Copy link
Member

If you want SVG content included in pre-rendered HTML, you are using wrong component. Both these components are designed to load icon data on demand. One of core functionalities is that nothing is rendered on server, icon data is not sent from server, but loaded from Iconify API as needed.

Solution is to use Unplugin Icons instead. It generates simple components, which are rendered on server: https://github.com/antfu/unplugin-icons

@mattrossman
Copy link

I wish there was a solution that wasn't quite so intrusive to use.

Although it renders the desired SVG output, with Unplugin Icons I have to:

  • Inject a plugin into Webpack's config in my next.config.js
  • Modify my tsconfig.json to point to their types (issue)
  • Import individual icons from a non-standard ~icons/ alias with no intellisense assistance
    • and explicity specify .jsx extension on imports for Next.js

Since it uses a Webpack plugin, it's also not compatible with Next.js's --turbo mode.

It's one of those things you add to a project and then forget how to work around its quirks a month later. What I'd really like is a regular React component that "just works" with Iconify's icon sets without any additional compiler config or special import syntax.

@cyberalien
Copy link
Member

Would something simple, like @iconify-react/logos package (and similar packages for other icon sets) with exported JSX for each icon solve this?

Then usage would be like this:

import { DiscordIcon } from '@iconify-react/logos';

function Whatever() {
  return <div><DiscordIcon /></div>;
}

or something like that (suggestions are welcome)?

@sthill1001rues
Copy link

sthill1001rues commented Oct 10, 2023

I am facing the exact same problem and as the issue is open and the conversation ended up with a question.
Yes this kind of implementation would be great if we can have the icons pre-rendered and no pop-in of the icons at page refresh.
(I precise I use "use client"; as it is not a problem to me)

@XFBC
Copy link

XFBC commented Oct 22, 2023

I was showing an error when rendering on the client side, I used 'use client', it resolved it!

@carlosyan1807
Copy link

carlosyan1807 commented Nov 16, 2023

Here are a few notes on what I've tried.

  1. unplugin-icons
    The icon renders correctly in server and client components, but will receive webpack cache warning at startup.

  2. @iconify/tailwind
    With css injected into the page, both server components and client components render correctly.
    The css class names must follow tailwindcss format, and the IconifyIntelliSense plugin won't recognize them all correctly.

image
  1. Use @iconify/tools and @iconify/utils to create an icons bundle, and use Icon component from @iconify/react/dist/offline to rendering.
    Before nextjs 13 App Router, just import icons bundle in pages/_app.tsx.
    Use App Router, import icons bundle in RootLayout, only icons in server componentss can be rendered.
    The client component won't render because it doesn''t import addCollection.
import '@/lib/icons-bundle/icons-bundle'
import { Icon as IconifyIcon, type IconProps } from '@iconify/react/dist/offline'

export const Icon = ({ icon, ...rest }: IconProps) => {
  return <IconifyIcon icon={icon} data-svg-icon="" width="1.2em" height="1.2em" {...rest} />
}

I have now import the icons bundle in icon component file, icon renders currectly in server and client components.
However, the icon bundle file will be packaged into the client JS by nextjs, which is bulky, and it will be a copy of both layout.js and page.js. So this may not be the right way.
I couldn't find a way to support render both server and client components, and load them on demand.

If anyone has tried anything else, I'd love to know.

@carlosyan1807
Copy link

Update:

I created an empty component that uses use client and imported icons-bundle file.
It provides the right addCollection for client components to render icons on server side and doesn't package entire icons-bundle into client JS.
Still need to import icons-bundle in RootLayout to render icons for server components.

/components/providers/icons-bundle.provider.tsx

'use client'

import '@/lib/icons-bundle/icons-bundle' // for client components

export function IconsBundleProvider() {
  return <></>
}

/app/layout.tsx

import '@/lib/icons-bundle/icons-bundle' // for server components

export default function RootLayout({ children }: React.PropsWithChildren) {
  return (
    <html lang="en">
      <body>
        {children}
        <IconsBundleProvider />
      </body>
    </html>
  )
}

/lib/icons-bundle/icons-bundle.js
image

@heyask
Copy link

heyask commented Mar 5, 2024

okay, but the problem in SSR or Next.js app router is layout shift.

layout-shift.mov

so I simply wrapped import { Icon } from '@iconify/react' to wrapper component to avoid it.
for those who are experiencing problems, here is the code.

Icon.tsx

import React, { useEffect, useState } from 'react';
import { Icon as RealIcon, IconProps } from '@iconify/react';

export function Icon(props: IconProps) {
  const [mounted, setMounted] = useState<boolean>(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  return mounted ? (
    <RealIcon {...props} />
  ) : (
    <span
      style={{
        width: props.width || 20,
        height: props.width || 20,
      }}
    >
      <RealIcon {...props} />
    </span>
  );
}

Result

no-layout-shift.mov

@cyberalien
Copy link
Member

cyberalien commented Apr 28, 2024

Published new version of @iconify/react that is compatible with Next.js.

Currently available as @iconify/react@next. It is a full rewrite of icon component, so need to do more testing before can mark it as stable, but so far works fine in my tests.

Long overdue. Sorry for delay.

It can render icons on server side if data is provided. You can provide data by using loadIcon Promise to load icon data before rendering it and adding ssr={true} attribute to icon: <Icon icon="mdi:home" ssr={true} />. Make sure data is available on client side too when doing this, otherwise hydration might fail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

7 participants