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

TextDecoder class is a global reference for import { TextDecoder } from 'node:util' 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

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 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 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

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

TextEncoder class is a global reference for import { TextEncoder } from 'node:util' 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

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

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

TransformStream class is a global reference for import { TransformStream } from 'node:stream/web'. https://nodejs.org/api/globals.html#class-transformstream

@sincev18.0.0
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

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 (this includes browsers, Deno, and some modern edge runtimes).

@seeAPI
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 (this includes browsers, Deno, and some modern edge runtimes).

@seeAPI
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

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

pipeThrough
(const injectAntdStyles: anyinjectAntdStyles), {
ResponseInit.headers?: HeadersInit | undefinedheaders: responseHeaders: HeadersresponseHeaders, ResponseInit.status?: number | undefinedstatus: responseStatusCode: numberresponseStatusCode, }); }

References