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

Fix SSR of Icon component #1200

Merged
merged 4 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/web-react/config/tsconfig.prod.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "es2015",
"module": "es2020",
"target": "es2015",
"noEmit": false
},
Expand Down
2 changes: 1 addition & 1 deletion packages/web-react/config/vite/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { defineConfig } from 'vite';
import handlebars from 'vite-plugin-handlebars';
import { getNestedDirs } from '../../scripts/build';

const hiddenDemoComponents = ['Field', 'Dialog', 'Icon', 'TextFieldBase', 'VisuallyHidden'];
const hiddenDemoComponents = ['Field', 'Dialog', 'Icon', 'NoSrr', 'TextFieldBase', 'VisuallyHidden'];

export default defineConfig({
server: SERVERS.DEVELOPMENT['web-react'],
Expand Down
4 changes: 2 additions & 2 deletions packages/web-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@
"examples:build": "vite build --config ./config/vite/app.ts",
"examples:preview": "vite preview --config ./config/vite/app.ts",
"prebuild": "shx rm -rf dist && shx mkdir -p dist && shx cp package.json README.md dist/ ",
"build": "npm-run-all --serial build:umd build:es2015 build:esNext",
"build": "npm-run-all --serial build:umd build:es2020 build:esNext",
"build:cjs": "yarn rollup --bundleConfigAsCjs",
"build:umd": "npm-run-all --serial webpack:dev webpack:prod",
"build:es2015": "tsc --module es2015 --outDir dist --project ./config/tsconfig.prod.json",
"build:es2020": "tsc --module es2020 --outDir dist --project ./config/tsconfig.prod.json",
"build:esNext": "echo tsc --module esNext --outDir dist/_esNext --project ./config/tsconfig.prod.json",
"postbuild": "npm-run-all prepdist resolve build:cjs",
"prepdist": "node ./scripts/prepareDist.js",
Expand Down
1 change: 1 addition & 0 deletions packages/web-react/scripts/entryPoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const entryPoints = [
{ dirs: ['components', 'Item'] },
{ dirs: ['components', 'Link'] },
{ dirs: ['components', 'Modal'] },
{ dirs: ['components', 'NoSsr'] },
{ dirs: ['components', 'Pagination'] },
{ dirs: ['components', 'Pill'] },
{ dirs: ['components', 'Radio'] },
Expand Down
59 changes: 41 additions & 18 deletions packages/web-react/src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,69 @@ import React from 'react';
import { useIcon, useStyleProps } from '../../hooks';
import { IconProps } from '../../types';
import { htmlParser } from '../../utils/htmlParser';
import { NoSsr } from '../NoSsr';

const defaultProps = {
ariaHidden: true,
boxSize: 24,
};

export const Icon = (props: IconProps): JSX.Element => {
export const Icon = (props: IconProps) => {
const { boxSize, name, title, ariaHidden, ...restProps } = props;
let icon = useIcon(name);
const { styleProps, props: otherProps } = useStyleProps(restProps);
const isHtmlParserLoaded = typeof htmlParser === 'function';

if (title) {
icon = `<title>${title}</title>${icon}`;
}

// @deprecated Usage of `html-react-parser` will be required in the next major version.
if (typeof window === 'undefined' && htmlParser == null) {
if (htmlParser == null) {
warning(
false,
'Icon component is not supported in SSR without use of `html-react-parser`. Please install, missing peer dependency.',
);
}

if (isHtmlParserLoaded) {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Incompatible HTMLElement and SVGSVGElement
<svg
viewBox="0 0 24 24"
fill="none"
width={boxSize}
height={boxSize}
aria-hidden={ariaHidden}
{...otherProps}
{...styleProps}
className={styleProps.className}
>
{/* @ts-expect-error TS2349: This expression is not callable. Type 'never' has no call signatures. */}
{htmlParser(icon)}
</svg>
);
}

return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Incompatible HTMLElement and SVGSVGElement
<svg
viewBox="0 0 24 24"
fill="none"
width={boxSize}
height={boxSize}
aria-hidden={ariaHidden}
// @deprecated Usage of dangerouslySetInnerHTML is discouraged due to support of SSR and will be removed in the next major version.
{...(typeof window !== 'undefined' ? { dangerouslySetInnerHTML: { __html: icon } } : {})}
{...otherProps}
{...styleProps}
className={styleProps.className}
>
{typeof window === 'undefined' && typeof htmlParser === 'function' ? htmlParser(icon) : null}
</svg>
<NoSsr>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore: Incompatible HTMLElement and SVGSVGElement */}
<svg
viewBox="0 0 24 24"
fill="none"
width={boxSize}
height={boxSize}
aria-hidden={ariaHidden}
// @deprecated Usage of dangerouslySetInnerHTML is discouraged due to support of SSR and will be removed in the next major version.
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: icon }}
{...otherProps}
{...styleProps}
className={styleProps.className}
/>
</NoSsr>
);
};

Expand Down
17 changes: 17 additions & 0 deletions packages/web-react/src/components/NoSsr/NoSsr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect, useState } from 'react';
import { ChildrenProps } from '../../types';

interface NoSsrProps extends ChildrenProps {}

const NoSsr = ({ children }: NoSsrProps): JSX.Element => {
const [isMounted, setMount] = useState(false);

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

// @ts-expect-error Type 'ReactNode' is not assignable to type 'Element'.
return isMounted ? children : null;
};

export default NoSsr;
19 changes: 19 additions & 0 deletions packages/web-react/src/components/NoSsr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# NoSsr

The NoSsr component disables rendering of any wrapped component by server prerender.

```jsx
import { NoSsr } from '@lmc-eu/spirit-web-react/components';
```

Basic example usage:

```jsx
<NoSsr>This is never prerendered</NoSsr>
```

### API

| Name | Type | Default | Required | Description |
| ---------- | ----------- | ------- | -------- | ------------ |
| `children` | `ReactNode` | — | ✔ | Wrapped node |
1 change: 1 addition & 0 deletions packages/web-react/src/components/NoSsr/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as NoSsr } from './NoSsr';
1 change: 1 addition & 0 deletions packages/web-react/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * from './Icon';
export * from './Item';
export * from './Link';
export * from './Modal';
export * from './NoSsr';
export * from './Pagination';
export * from './Pill';
export * from './Radio';
Expand Down
15 changes: 13 additions & 2 deletions packages/web-react/src/utils/htmlParser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import parse from 'html-react-parser';
let parse = null;

export const htmlParser = typeof window === 'undefined' ? parse : null;
import('html-react-parser')
.then((htmlReactParser) => {
parse = htmlReactParser;
})
.catch(() =>
// eslint-disable-next-line no-console
console.warn(
'`html-react-parser` is not installed and will be required in the next major version. Please install, missing peer dependency.',
),
);

export const htmlParser = typeof parse === 'function' ? parse : null;