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

Closed
AestasLonewolf opened this issue Jul 11, 2023 · 31 comments
Closed

Render icons on server side in NextJS 13 #234

AestasLonewolf opened this issue Jul 11, 2023 · 31 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.

@cyberalien
Copy link
Member

Version 5 is now stable and published with latest tag.

Works correctly with Next.js, no longer requires a wrapper component.

@tjx666
Copy link

tjx666 commented May 31, 2024

I using the page router, but why still server return empty span?

@cyberalien

reproduce online stackblitz

image

@cyberalien
Copy link
Member

First code should work, so not sure about that.

In second and third example you are providing bad data. Second is incomplete data, third is just wrong data.

@tjx666
Copy link

tjx666 commented May 31, 2024

The first one is also no svg rendered in server:

image

@cyberalien
Copy link
Member

That's normal behavior. Component is client only because Next throws an error when it encounters useState in a component, so stateful React components cannot be used in Next.

However, ssr not working is a bug. I've just ran few tests, it is indeed failing for some reason.

@tjx666
Copy link

tjx666 commented May 31, 2024

Sorry for so many questions, I indeed confused one day.

What's the meaning of icon data?

Why my second usage case is bad data? The expected data is svg or json?

image

@cyberalien
Copy link
Member

Data is IconifyIcon object: https://iconify.design/docs/types/iconify-icon.html

To extract it from icon set, use getIconData() from Iconify Utils: https://iconify.design/docs/libraries/utils/get-icon-data.html

@tjx666
Copy link

tjx666 commented May 31, 2024

That's normal behavior. Component is client only because Next throws an error when it encounters useState in a component, so stateful React components cannot be used in Next.

Thanks for quickly answer.

My usecase is next page router, not app router. My thought is iconify had already get the icon data from @iconify-json/ph/icons.json, so it should have ability to output the svg in server

@cyberalien
Copy link
Member

Found what's causing it: initial state was set to empty even though ssr was enabled.

Fixed in 5.0.1

@cyberalien
Copy link
Member

Sorry for so many questions, I indeed confused one day.

What's the meaning of icon data?

Updated docs to link to type.

@aleynaeser
Copy link

aleynaeser commented Jun 1, 2024

Sorry for so many questions, I indeed confused one day.
What's the meaning of icon data?

Updated docs to link to type.

Hi @cyberalien, i am using Iconify in Nextjs. ssr is true and called like <Icon icon="cil:paper-plane" ssr={true} />

But still icon rendering after page loading. It does not look nice. Its inital value is like empty

Version => "@iconify/react": "^5.0.1",

Ekran.Kaydi.2024-06-01.20.39.08.mov

@cyberalien
Copy link
Member

This might happen if either:

  • icon data is not available immediately (should be provided)
  • script is still loading

Component cannot render on server because of Next.js being a mess.

Attribute is named that for consistency with Vue version of component. Unlike Next, Nuxt renders components correctly on server, so attribute works as expected. It can even load data on server from API if needed (using loadIcon function). Next is just a mess: it does not support class based components, it does not support useState, which means there is no way to use normal components in Next, so library must be a client only library.

If you want to render icon immediately without any shenanigans, best solution is to use something else, like Unplugin Icons (renders as SVG), UnoCSS with icons preset or Tailwind CSS plugin. Also Iconify Icon web component works very well (I'm using it on Iconify website), but requires adding one line to CSS to avoid content shift when web component is being loaded.

@cyberalien
Copy link
Member

@aleynaeser
Copy link

Thank you for your answer. I try to use '@iconify-icon/react'; web component. It does not show icons in server side components. And in the client component it still loading after. So it is support render icons in server side?And why does nıot work in server component? @cyberalien

@cyberalien
Copy link
Member

Web component renders icon in Shadow DOM. Web components work independent of React, so all React sees is <iconify-icon icon="mdi:home"></iconify-icon>, not actual icon code.

In browser web component cannot work until browser loads web component, so there might be a small delay if browser needs to load a big bundle.

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

9 participants