Loading...

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 : any
Obtain the return type of a function type
ReturnType
<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) => 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#textdecoder
@sincev11.0.0
TextDecoder
().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: 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.
@paramsearchValue A string or regular expression to search for.@paramreplaceValue A string containing the text to replace. When the {@linkcode searchValue} is a `RegExp`, all matches are replaced if the `g` flag is set (or only those matches at the beginning, if the `y` flag is also present). Otherwise, only the first match of {@linkcode searchValue} is replaced.
replace
(
class InjectAntDSSRStylesInjectAntDSSRStyles.InjectAntDSSRStyles.stylesSigill: stringstylesSigill, this.InjectAntDSSRStyles.stylesToInject: anystylesToInject ); 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#textencoder
@sincev11.0.0
TextEncoder
().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(): 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).
@see[API](https://reactjs.org/docs/react-dom-server.html#rendertoreadablestream)
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).
@see[API](https://reactjs.org/docs/react-dom-server.html#rendertoreadablestream)
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) => 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: 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, }); }

References