I recently had reasons to use the Ant design component library with a remix run project mostly because of the well-documented, practical components the library provides. However, support for integrating ant design with any framework (remix in my case) that encourages rendering through streaming, is non-existent. While Ant Design’s documentation outlines steps for Vite (Remix’s build tool of choice), however, this approach resulted in FOUC1. I wanted something better than what was suggested that particularly catered to the streaming rendering paradigm remix run uses.
I couldn’t find any guidance on how one might go about integrating ant design, that is till I stumbled on a post2 that discussed how one might go about supporting serverside rendering of ant design styles if one was rendering React to string. This then inspired me to adapt the principles suggested in the post to support React’s streaming paradigm.
We’ll want to create a helper class that will consolidate everything that relates to injecting server rendered styles into the document.
// @ts-expect-error -- this import errors becuase @ant-design/cssinjs is not installed
import { import extractStyle
extractStyle } from '@ant-design/cssinjs';
export class class InjectAntDSSRStyles
InjectAntDSSRStyles {
static InjectAntDSSRStyles.stylesSigill: string
stylesSigill = '__ANTD__';
constructor(private InjectAntDSSRStyles.stylesToInject: any
stylesToInject: type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
Obtain the return type of a function typeReturnType<typeof import extractStyle
extractStyle>) {
this.InjectAntDSSRStyles.stylesToInject: any
stylesToInject = stylesToInject: any
stylesToInject;
}
InjectAntDSSRStyles.createTransformStream(): TransformStream<any, any>
createTransformStream() {
const const transformer: Transformer<any, any>
transformer: interface Transformer<I = any, O = any>
Transformer = {
Transformer<any, any>.start?: TransformerStartCallback<any> | undefined
start() {},
Transformer<any, any>.transform?: TransformerTransformCallback<any, any> | undefined
transform: (chunk: any
chunk, controller: TransformStreamDefaultController<any>
controller) => {
const const text: string
text = new var TextDecoder: new (label?: string, options?: TextDecoderOptions) => TextDecoder
The **`TextDecoder`** interface represents a decoder for a specific text encoding, such as `UTF-8`, `ISO-8859-2`, `KOI8-R`, `GBK`, etc.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder)
`TextDecoder` class is a global reference for `require('util').TextDecoder`
https://nodejs.org/api/globals.html#textdecoderTextDecoder().TextDecoder.decode(input?: AllowSharedBufferSource, options?: TextDecodeOptions): string
The **`TextDecoder.decode()`** method returns a string containing text decoded from the buffer passed as a parameter.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode)decode(chunk: any
chunk);
const const replaced: string
replaced = const text: string
text.String.replace(searchValue: string | RegExp, replaceValue: string): string (+3 overloads)
Replaces text in a string, using a regular expression or search string.replace(
class InjectAntDSSRStyles
InjectAntDSSRStyles.InjectAntDSSRStyles.stylesSigill: string
stylesSigill,
this.InjectAntDSSRStyles.stylesToInject: any
stylesToInject
);
controller: TransformStreamDefaultController<any>
controller.TransformStreamDefaultController<any>.enqueue(chunk?: any): void
The **`enqueue()`** method of the TransformStreamDefaultController interface enqueues the given chunk in the readable side of the stream.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/enqueue)enqueue(new var TextEncoder: new () => TextEncoder
The **`TextEncoder`** interface takes a stream of code points as input and emits a stream of UTF-8 bytes.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder)
`TextEncoder` class is a global reference for `require('util').TextEncoder`
https://nodejs.org/api/globals.html#textencoderTextEncoder().TextEncoder.encode(input?: string): Uint8Array<ArrayBuffer>
The **`TextEncoder.encode()`** method takes a string as input, and returns a Global_Objects/Uint8Array containing the text given in parameters encoded with the specific method for that TextEncoder object.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encode)encode(const replaced: string
replaced));
},
Transformer<any, any>.flush?: TransformerFlushCallback<any> | undefined
flush(controller: TransformStreamDefaultController<any>
controller) {
controller: TransformStreamDefaultController<any>
controller.TransformStreamDefaultController<any>.terminate(): void
The **`terminate()`** method of the TransformStreamDefaultController interface closes the readable side and errors the writable side of the stream.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/terminate)terminate();
},
};
return new var TransformStream: new <any, any>(transformer?: Transformer<any, any> | undefined, writableStrategy?: QueuingStrategy<any> | undefined, readableStrategy?: QueuingStrategy<any> | undefined) => TransformStream<any, any>
The **`TransformStream`** interface of the Streams API represents a concrete implementation of the pipe chain _transform stream_ concept.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream)TransformStream(const transformer: Transformer<any, any>
transformer);
}
}
In our root.tsx
(or equivalent) file, we would then use the static property stylesSigill
on our predefined InjectAntDSSRStyles
class to create a placeholder in the html document that would get streamed to the user from the server like so;
// @ts-expect-error -- this errors because InjectAntDSSRStyles is expected to be imported
typeof var document: Document
**`window.document`** returns a reference to the document contained in the window.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)document === 'undefined' ? InjectAntDSSRStyles.stylesSigill : ''
Given we’ve followed the steps above, next we use the InjectAntDSSRStyles
class in our server’s handleRequest
(or equivalent) function to inject the styles extracted from the document to be rendered into the response stream that will be sent to the user, below is a diff of how the handleRequest
function would look like after we’ve integrated our helper class;
import React from 'react';
import { function renderToReadableStream(children: React.ReactNode, options?: RenderToReadableStreamOptions): Promise<ReactDOMServerReadableStream>
Only available in the environments with [Web Streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) (this includes browsers, Deno, and some modern edge runtimes).renderToReadableStream } from 'react-dom/server';
// @ts-expect-error -- this import returns an error becuase @ant-design/cssinjs is not installed
import { import createCache
createCache, import extractStyle
extractStyle, import StyleProvider
StyleProvider } from '@ant-design/cssinjs';
export default async function function handleRequest(request: Request, responseStatusCode: number, responseHeaders: Headers, ...rest: unknown[]): Promise<Response>
handleRequest(
request: Request
request: Request,
responseStatusCode: number
responseStatusCode: number,
responseHeaders: Headers
responseHeaders: Headers,
...rest: unknown[]
rest: unknown[] // other parameters specific to your remix run setup
) {
// we'd want to create this cache before constructing the
// react tree that'll be rendered
const const antDStyleCache: any
antDStyleCache = import createCache
createCache();
const const body: ReactDOMServerReadableStream
body = await function renderToReadableStream(children: React.ReactNode, options?: RenderToReadableStreamOptions): Promise<ReactDOMServerReadableStream>
Only available in the environments with [Web Streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) (this includes browsers, Deno, and some modern edge runtimes).renderToReadableStream(
// we include the cache in the StyleProvider component
<import StyleProvider
StyleProvider cache: any
cache={const antDStyleCache: any
antDStyleCache}>
...
import StyleProvider
StyleProvider>,
);
// @ts-expect-error -- this errors because InjectAntDSSRStyles is expected to be imported
const const injectAntdStyles: any
injectAntdStyles = new InjectAntDSSRStyles(
import extractStyle
extractStyle(const antDStyleCache: any
antDStyleCache)
).createTransformStream();
// finally we pipe the stream response through the util we created
return new var Response: new (body?: BodyInit | null, init?: ResponseInit) => Response
The **`Response`** interface of the Fetch API represents the response to a request.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Response)Response(const body: ReactDOMServerReadableStream
body.ReadableStream<any>.pipeThrough<any>(transform: ReadableWritablePair<any, any>, options?: StreamPipeOptions): ReadableStream<any>
The **`pipeThrough()`** method of the ReadableStream interface provides a chainable way of piping the current stream through a transform stream or any other writable/readable pair.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeThrough)pipeThrough(const injectAntdStyles: any
injectAntdStyles), {
ResponseInit.headers?: HeadersInit | undefined
headers: responseHeaders: Headers
responseHeaders,
ResponseInit.status?: number | undefined
status: responseStatusCode: number
responseStatusCode,
});
}