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 extractStyleextractStyle } from '@ant-design/cssinjs';
export class class InjectAntDSSRStylesInjectAntDSSRStyles {
static InjectAntDSSRStyles.stylesSigill: stringstylesSigill = '__ANTD__';
constructor(private InjectAntDSSRStyles.stylesToInject: anystylesToInject: type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : anyObtain the return type of a function typeReturnType<typeof import extractStyleextractStyle>) {
this.InjectAntDSSRStyles.stylesToInject: anystylesToInject = stylesToInject: anystylesToInject;
}
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> | undefinedstart() {},
Transformer<any, any>.transform?: TransformerTransformCallback<any, any> | undefinedtransform: (chunk: anychunk, controller: TransformStreamDefaultController<any>controller) => {
const const text: stringtext = new var TextDecoder: new (label?: string, options?: TextDecoderOptions) => TextDecoderThe **`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): stringThe **`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: anychunk);
const const replaced: stringreplaced = const text: stringtext.String.replace(searchValue: string | RegExp, replaceValue: string): string (+3 overloads)Replaces text in a string, using a regular expression or search string.replace(
class InjectAntDSSRStylesInjectAntDSSRStyles.InjectAntDSSRStyles.stylesSigill: stringstylesSigill,
this.InjectAntDSSRStyles.stylesToInject: anystylesToInject
);
controller: TransformStreamDefaultController<any>controller.TransformStreamDefaultController<any>.enqueue(chunk?: any): voidThe **`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 () => TextEncoderThe **`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: stringreplaced));
},
Transformer<any, any>.flush?: TransformerFlushCallback<any> | undefinedflush(controller: TransformStreamDefaultController<any>controller) {
controller: TransformStreamDefaultController<any>controller.TransformStreamDefaultController<any>.terminate(): voidThe **`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 createCachecreateCache, import extractStyleextractStyle, import StyleProviderStyleProvider } from '@ant-design/cssinjs';
export default async function function handleRequest(request: Request, responseStatusCode: number, responseHeaders: Headers, ...rest: unknown[]): Promise<Response>handleRequest(
request: Requestrequest: Request,
responseStatusCode: numberresponseStatusCode: number,
responseHeaders: HeadersresponseHeaders: 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: anyantDStyleCache = import createCachecreateCache();
const const body: ReactDOMServerReadableStreambody = 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 StyleProviderStyleProvider cache: anycache={const antDStyleCache: anyantDStyleCache}>
...
import StyleProviderStyleProvider>,
);
// @ts-expect-error -- this errors because InjectAntDSSRStyles is expected to be imported
const const injectAntdStyles: anyinjectAntdStyles = new InjectAntDSSRStyles(
import extractStyleextractStyle(const antDStyleCache: anyantDStyleCache)
).createTransformStream();
// finally we pipe the stream response through the util we created
return new var Response: new (body?: BodyInit | null, init?: ResponseInit) => ResponseThe **`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: ReactDOMServerReadableStreambody.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: anyinjectAntdStyles), {
ResponseInit.headers?: HeadersInit | undefinedheaders: responseHeaders: HeadersresponseHeaders,
ResponseInit.status?: number | undefinedstatus: responseStatusCode: numberresponseStatusCode,
});
}