Skip to content

Commit c900765

Browse files
author
Kalle Ott
committed
feat: enable spritesheet creation outside of the rendered app
1 parent 2a9ad73 commit c900765

File tree

6 files changed

+110
-94
lines changed

6 files changed

+110
-94
lines changed

.github/workflows/npm-publish.yml

Lines changed: 0 additions & 47 deletions
This file was deleted.

.github/workflows/test.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: Node.js Package
2+
3+
on: push
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v2
10+
- uses: actions/setup-node@v1
11+
with:
12+
node-version: 12
13+
- run: yarn --pure-lockfile
14+
- run: yarn test

example/src/server.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import fastify from 'fastify';
44
import { renderToString } from 'react-dom/server';
55
import fastifyStatic from 'fastify-static';
66
import {
7-
renderSpriteSheetToString,
87
SpriteContextProvider,
98
IconsCache,
9+
createSpriteSheetString,
1010
} from 'react-lazy-svg';
1111
import { readSvg } from './serverLoadSvg';
1212

@@ -25,9 +25,11 @@ server.get('/*', async (_, res) => {
2525
const markup = renderToString(
2626
<SpriteContextProvider loadSVG={readSvg} knownIcons={sessionIcons}>
2727
<App />
28-
</SpriteContextProvider>
28+
</SpriteContextProvider>,
2929
);
3030

31+
const spriteSheet = await createSpriteSheetString(sessionIcons);
32+
3133
if (context.url) {
3234
res.redirect(context.url);
3335
} else {
@@ -51,18 +53,11 @@ server.get('/*', async (_, res) => {
5153
</head>
5254
<body>
5355
<div id="root">${markup}</div>
56+
${spriteSheet}
5457
</body>
5558
</html>`;
5659

57-
const extended = await renderSpriteSheetToString(
58-
renderedHtml,
59-
sessionIcons
60-
);
61-
62-
res
63-
.type('text/html')
64-
.status(200)
65-
.send(extended);
60+
res.type('text/html').status(200).send(renderedHtml);
6661
}
6762
});
6863

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"url": "https://github.com/kaoDev/"
1010
},
1111
"description": "react-lazy-svg is a simple way to use SVGs with the performance benefits of a sprite-sheet and svg css styling possibilities. Without bloating the bundle. It automatically creates a sprite-sheet for all used SVGs on the client but also provides a function to create a server side rendered sprite-sheet for icons used in the first paint.",
12-
"version": "1.0.1",
12+
"version": "1.1.0",
1313
"main": "dist/index.js",
1414
"module": "dist/react-lazy-svg.esm.js",
1515
"typings": "dist/index.d.ts",

src/index.tsx

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,32 @@ import React, {
66
useEffect,
77
useCallback,
88
useMemo,
9+
useRef,
910
} from 'react';
1011
import { renderToStaticMarkup } from 'react-dom/server';
12+
import { createPortal } from 'react-dom';
1113

12-
const spriteSheetId = '__SVG_SPRITE_SHEET__';
13-
14-
const ssrEmptySpriteSheet = `<svg id="${spriteSheetId}" style="display:none"></svg>`;
14+
const internalSpriteSheetId = '__SVG_SPRITE_SHEET__';
15+
const isSSR = typeof document === 'undefined';
1516

1617
const globalIconsCache: IconsCache = new Map();
1718

18-
export const renderSpriteSheetToString = async (
19-
markupString: string,
20-
knownIcons: IconsCache,
21-
) => {
19+
export const createSpriteSheetString = async (knownIcons: IconsCache) => {
2220
const arr = await Promise.all(Array.from(knownIcons.values()));
2321

24-
const spriteSheet = renderToStaticMarkup(
22+
return renderToStaticMarkup(
2523
<SpriteSheet icons={arr.filter((a): a is IconData => a != null)} />,
2624
);
25+
};
26+
27+
export const renderSpriteSheetToString = async (
28+
markupString: string,
29+
knownIcons: IconsCache,
30+
spriteSheetId = internalSpriteSheetId,
31+
) => {
32+
const spriteSheet = await createSpriteSheetString(knownIcons);
2733

34+
const ssrEmptySpriteSheet = `<svg id="${spriteSheetId}" style="display:none"></svg>`;
2835
return markupString.replace(ssrEmptySpriteSheet, spriteSheet);
2936
};
3037

@@ -135,12 +142,14 @@ export interface SpriteContext {
135142
*/
136143
loadSVG: (url: string) => Promise<string | undefined>;
137144
knownIcons?: IconsCache;
145+
embeddedSSR?: boolean;
138146
}
139147

140148
export const SpriteContextProvider: FC<SpriteContext> = ({
141149
children,
142150
loadSVG,
143151
knownIcons = globalIconsCache,
152+
embeddedSSR = false,
144153
}) => {
145154
const icons = useIcons();
146155

@@ -162,7 +171,7 @@ export const SpriteContextProvider: FC<SpriteContext> = ({
162171
return (
163172
<spriteContext.Provider value={contextValue}>
164173
{children}
165-
<SpriteSheet icons={icons}></SpriteSheet>
174+
{(!isSSR || embeddedSSR) && <SpriteSheet icons={icons}></SpriteSheet>}
166175
</spriteContext.Provider>
167176
);
168177
};
@@ -173,7 +182,7 @@ export const Icon: FC<{ url: string } & React.SVGProps<SVGSVGElement>> = ({
173182
}) => {
174183
const { registerSVG } = useContext(spriteContext);
175184

176-
if (typeof document === 'undefined') {
185+
if (isSSR) {
177186
registerSVG(url);
178187
} else {
179188
useEffect(() => {
@@ -189,31 +198,39 @@ export const Icon: FC<{ url: string } & React.SVGProps<SVGSVGElement>> = ({
189198
};
190199

191200
const hidden = { display: 'none' };
192-
const SpriteSheet: FC<{ icons: IconData[] }> = ({ icons }) => {
201+
const SpriteSheet: FC<{
202+
icons: IconData[];
203+
spriteSheetId?: string;
204+
}> = ({ icons, spriteSheetId = internalSpriteSheetId }) => {
205+
const spriteSheetContainer = useRef(
206+
!isSSR ? document.getElementById(spriteSheetId) : null,
207+
);
208+
209+
const renderedIcons = icons.map(
210+
({
211+
id,
212+
svgString,
213+
attributes: { width, height, ['xmlns:xlink']: xmlnsXlink, ...attributes },
214+
}) => {
215+
return (
216+
<symbol
217+
key={id}
218+
id={id}
219+
xmlnsXlink={xmlnsXlink}
220+
{...attributes}
221+
dangerouslySetInnerHTML={svgString}
222+
/>
223+
);
224+
},
225+
);
226+
227+
if (spriteSheetContainer.current) {
228+
return createPortal(renderedIcons, spriteSheetContainer.current);
229+
}
230+
193231
return (
194232
<svg id={spriteSheetId} style={hidden}>
195-
{icons.map(
196-
({
197-
id,
198-
svgString,
199-
attributes: {
200-
width,
201-
height,
202-
['xmlns:xlink']: xmlnsXlink,
203-
...attributes
204-
},
205-
}) => {
206-
return (
207-
<symbol
208-
key={id}
209-
id={id}
210-
xmlnsXlink={xmlnsXlink}
211-
{...attributes}
212-
dangerouslySetInnerHTML={svgString}
213-
/>
214-
);
215-
},
216-
)}
233+
{renderedIcons}
217234
</svg>
218235
);
219236
};
@@ -228,7 +245,10 @@ const mapNodeAttributes = (rawAttributes: NamedNodeMap) =>
228245
{},
229246
);
230247

231-
export const initOnClient = (knownIcons: IconsCache = globalIconsCache) => {
248+
export const initOnClient = (
249+
knownIcons: IconsCache = globalIconsCache,
250+
spriteSheetId = internalSpriteSheetId,
251+
) => {
232252
knownIcons.clear();
233253
const spriteSheet = document.getElementById(spriteSheetId);
234254

test/ssr.test.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SpriteContextProvider,
88
IconsCache,
99
renderSpriteSheetToString,
10+
createSpriteSheetString,
1011
} from '../src/index';
1112
import { renderToString } from 'react-dom/server';
1213

@@ -31,7 +32,7 @@ const loadSVG = async (url: string) => {
3132
test('render loaded svgs to a svg sprite sheet string', async () => {
3233
const cache: IconsCache = new Map();
3334
const renderedString = renderToString(
34-
<SpriteContextProvider knownIcons={cache} loadSVG={loadSVG}>
35+
<SpriteContextProvider embeddedSSR knownIcons={cache} loadSVG={loadSVG}>
3536
<Icon url={'1'}></Icon>
3637
</SpriteContextProvider>,
3738
);
@@ -45,3 +46,36 @@ test('render loaded svgs to a svg sprite sheet string', async () => {
4546
`"<svg><use xlink:href=\\"#1\\"></use></svg><svg id=\\"__SVG_SPRITE_SHEET__\\" style=\\"display:none\\"><symbol id=\\"1\\" xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 24 24\\"><path d=\\"M0 0h24v24H0z\\" fill=\\"none\\"/></symbol></svg>"`,
4647
);
4748
});
49+
50+
test('should not render an embedded sprite sheet when not explicitly asked for', async () => {
51+
const cache: IconsCache = new Map();
52+
const renderedString = renderToString(
53+
<SpriteContextProvider knownIcons={cache} loadSVG={loadSVG}>
54+
<Icon url={'1'}></Icon>
55+
</SpriteContextProvider>,
56+
);
57+
58+
const renderedSpriteSheet = await renderSpriteSheetToString(
59+
renderedString,
60+
cache,
61+
);
62+
63+
expect(renderedSpriteSheet).toMatchInlineSnapshot(
64+
`"<svg><use xlink:href=\\"#1\\"></use></svg>"`,
65+
);
66+
});
67+
68+
test('render loaded svgs to a svg sprite sheet string', async () => {
69+
const cache: IconsCache = new Map();
70+
renderToString(
71+
<SpriteContextProvider embeddedSSR knownIcons={cache} loadSVG={loadSVG}>
72+
<Icon url={'1'}></Icon>
73+
</SpriteContextProvider>,
74+
);
75+
76+
const renderedSpriteSheet = await createSpriteSheetString(cache);
77+
78+
expect(renderedSpriteSheet).toMatchInlineSnapshot(
79+
`"<svg id=\\"__SVG_SPRITE_SHEET__\\" style=\\"display:none\\"><symbol id=\\"1\\" xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 24 24\\"><path d=\\"M0 0h24v24H0z\\" fill=\\"none\\"/></symbol></svg>"`,
80+
);
81+
});

0 commit comments

Comments
 (0)