Description
Subject of the feature
Server rendering react-remark components by passing through an initial state value to useRemark, or using the component's children
.
Describe your issue here.
Problem
The useRemark hook's reactContent initial state is null. It looks like the only way to update this state is by using the exposed setMarkdownSource method.
The component sets the state of reactContent in a useEffect calling setMarkdownSource(children) which does not get executed on the server (by calling react-dom/server renderToString).
Alternatives
What are the alternative solutions? Please describe what else you have considered?
Have tried using the first example in README.md, which creates an infinite loop.
Looking through the source code, potential solutions:
setMarkdownSource uses unified.process which is async. We could create a synchronous function which uses unified.processSync to set the initial state based off a new prop in the useRemark hook e.g initialContent, though as I understand, if a provided remark plugin is async, this will throw an error.
I haven't tested this in a server rendered environment, however running this projects storybook seems to work ok with the following.
export const useRemark = ({
...,
initialContent,
}: UseRemarkOptions = {}): [ReactElement | null, (source: string) => void] => {
const initialParser = useCallback((source: string) => {
return unified()
.use(remarkParse, remarkParseOptions)
.use(remarkPlugins)
.use(remarkToRehype, remarkToRehypeOptions)
.use(rehypePlugins)
.use(rehypeReact, { createElement, Fragment, ...rehypeReactOptions })
.processSync(source).result as ReactElement;
}, []);
const [reactContent, setReactContent] = useState<ReactElement | null>(
() => initialContent ? initialParser(initialContent) : null
);
const setMarkdownSource = useCallback((source: string) => {
unified()
.use(remarkParse, remarkParseOptions)
.use(remarkPlugins)
.use(remarkToRehype, remarkToRehypeOptions)
.use(rehypePlugins)
.use(rehypeReact, { createElement, Fragment, ...rehypeReactOptions })
.process(source)
.then((vfile) => setReactContent(vfile.result as ReactElement))
.catch(onError);
}, []);
return [reactContent, setMarkdownSource];
};
export const Remark: FunctionComponent<RemarkProps> = ({
children,
...useRemarkOptions
}: RemarkProps) => {
const [reactContent, setMarkdownSource] = useRemark({
...useRemarkOptions,
initialContent: children,
});
useEffect(() => {
setMarkdownSource(children);
}, [children, setMarkdownSource]);
return reactContent;
};
For now, using react-markdown works fine (which seems to be synchronous) for our use case, however this project seems to align closer to our preference for parsing architecture).
Being able to server render initial content would be a useful feature.