Skip to content

Server rendering/setting initial markdown state #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mattywong opened this issue Jan 15, 2021 · 14 comments · Fixed by #18
Closed

Server rendering/setting initial markdown state #16

mattywong opened this issue Jan 15, 2021 · 14 comments · Fixed by #18
Labels
🙉 open/needs-info This needs some more info 🦋 type/enhancement This is great to have

Comments

@mattywong
Copy link

mattywong commented Jan 15, 2021

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.

@mattywong mattywong added 🙉 open/needs-info This needs some more info 🦋 type/enhancement This is great to have labels Jan 15, 2021
@ChristianMurphy
Copy link
Member

Server rendering react-remark components by passing through an initial state value to useRemark

What are you using for serverside rendering?
A framework like Gatsby or Next? Something else?

Have tried using the first example in README.md, which creates an infinite loop

Could you share a runnable example of this? for example in https://codesandbox.io

setMarkdownSource uses unified.process which is async

Could you expand why this is an problem?
Next, for example, supports async processes in SSR via getStaticProps https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation
Another consideration here, remark and rehype plugins can be async, and async plugins can only be used with process, I'd like to avoid the usage of processSync if possible to avoid restrictions on what plugins work.

to set the initial state based off a new prop in the useRemark hook e.g initialContent

an initialContent prop could be a good add.

For now, using react-markdown works fine (which seems to be synchronous) for our use case

FYI, react-markdown v6 will likely be built on top of react-remark.

@ChristianMurphy ChristianMurphy added 💬 type/discussion This is a request for comments 🦋 type/enhancement This is great to have 🧘 status/waiting and removed 🙉 open/needs-info This needs some more info 🦋 type/enhancement This is great to have 💬 type/discussion This is a request for comments labels Jan 15, 2021
@mattywong
Copy link
Author

Server rendering react-remark components by passing through an initial state value to useRemark

What are you using for serverside rendering?
A framework like Gatsby or Next? Something else?

We are server rendering in .NET Core via ReactJS.NET

Have tried using the first example in README.md, which creates an infinite loop

Could you share a runnable example of this? for example in https://codesandbox.io

Below example that crashes:

https://codesandbox.io/s/objective-montalcini-cixpt?

setMarkdownSource uses unified.process which is async

Could you expand why this is an problem?
Next, for example, supports async processes in SSR via getStaticProps https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation
Another consideration here, remark and rehype plugins can be async, and async plugins can only be used with process, I'd like to avoid the usage of processSync if possible to avoid restrictions on what plugins work.

We are using base react in a polyfilled Chakra core runtime on the server. AFAIK there is no way in base react to set an initial state via async in useState constructor (calling setState in a useEffect is not an option as that does not get run in react-dom/server's renderToString method). Perhaps a solution is to have an option to use processSync with process as the default? e.g a synchronous or async boolean option passed to useRemark hook? However we would still need a way to set the initial content state so react-dom/server can render it's contents.

@ChristianMurphy
Copy link
Member

ChristianMurphy commented Jan 18, 2021

https://codesandbox.io/s/objective-montalcini-cixpt this is the equivalent of:

function InfiniteLoop () {
  const [state, setState] = useState();
  setState({});
  return null;
}

each render setState is called, setState triggers a render, repeat ad infinitum.

a useEffect can be used to call set just once https://codesandbox.io/s/stoic-snow-4846x

We are server rendering in .NET Core via ReactJS.NET
in a polyfilled Chakra core runtime on the server

Oof, isomorphic react on a non-V8 runtime, with lightly supported language bindings, that doesn't sound fun, sorry.

Perhaps a solution is to have an option to use processSync with process as the default? e.g a synchronous or async boolean option passed to useRemark hook?

It is an option 🤔
And maybe one that could make sense.

Taking a step back from the problem, from a moment, it sounds like you're looking more for a pure static content generator?
Would going from markdown to javascript, and saving/running an ahead of time generated component work in your use case?
Something along the lines of: https://mdxjs.com/advanced/api (built on remark)

@mattywong
Copy link
Author

mattywong commented Jan 19, 2021

Taking a step back from the problem, from a moment, it sounds like you're looking more for a pure static content generator?
Would going from markdown to javascript, and saving/running an ahead of time generated component work in your use case?
Something along the lines of: https://mdxjs.com/advanced/api (built on remark)

Unfortunately not as the content is stored in a headless CMS. Currently it handles blog posts in markdown, but we are in the process of extending it out to handle custom pages as well (which may contain JSX and/or HTML), which will be handled by a content team and occasional developer for more complicated markup/layouts.

I'm also not sure if you'd be able to server render on nextjs either, though you're able to set initial component props in getStaticProps, this still doesn't give a pathway to setting the initial useRemark inner reactContent state (which is null useState(null) https://github.com/remarkjs/react-remark/blob/main/src/index.ts#L33). Though I haven't tested this in nextjs, my thinking is that it will still render null from the server, then after client hydration, the useEffect will run replacing with the parsed result from unified. Though you could use unified directly in getStaticProps to get it server rendered, however this sort of defeats the purpose of using this package.

EDIT:
I have got an example with nextjs as described above on codesandbox (open the result in a new browser window, disable JavaScript and the text will not be rendered):
https://codesandbox.io/s/0-no-persistent-layout-elements-forked-yh5pz?file=/pages/index.js

Result window:
https://yh5pz.sse.codesandbox.io/

@ChristianMurphy
Copy link
Member

ChristianMurphy commented Jan 21, 2021

I'm also not sure if you'd be able to server render on nextjs either

Not currently, I'm waiting for React Server Components and Serverside Suspense to shake out some more to allow for more flexible async components on the serverside.

Unfortunately not as the content is stored in a headless CMS. Currently it handles blog posts in markdown, but we are in the process of extending it out to handle custom pages as well (which may contain JSX and/or HTML), which will be handled by a content team and occasional developer for more complicated markup/layouts.

What would you think of

export interface UseRemarkOptions {
  remarkParseOptions?: Partial<RemarkParseOptions>;
  remarkToRehypeOptions?: RemarkRehypeOptions;
  rehypeReactOptions?: Partial<RehypeReactOptions<typeof createElement>>;
  remarkPlugins?: PluggableList;
  rehypePlugins?: PluggableList;
}

export const useRemarkSync = (
  source: string,
  {
    remarkParseOptions,
    remarkToRehypeOptions,
    rehypeReactOptions,
    remarkPlugins = [],
    rehypePlugins = [],
  }: UseRemarkOptions = {}
): ReactElement =>
  unified()
    .use(remarkParse, remarkParseOptions)
    .use(remarkPlugins)
    .use(remarkToRehype, remarkToRehypeOptions)
    .use(rehypePlugins)
    .use(rehypeReact, { createElement, Fragment, ...rehypeReactOptions })
    .processSync(source).result as ReactElement;

?

@mattywong
Copy link
Author

Looks good to me! Would it make sense to wrap it in a useMemo and have source as the dependency? Looks like it could potentially get expensive and block the main thread if the containing parent component gets re-rendered

@tremby
Copy link

tremby commented May 26, 2021

Popping my head in just to say that this is a blocker for me. I'm using Next.

@pedrosimao
Copy link

I just had same issue with SSG on Next.JS.
Using the <Remark> component would result in texts creating a javascript code instead of HTML tags on source code, which is bad for SEO.
So I ended up creating my own <Markdown> component with the code snippet from remark-react:

unified().use(parse).use(remark2react).processSync(children).result}

@ChristianMurphy
Copy link
Member

added in #18
documentation at https://github.com/remarkjs/react-remark#server-side-rendering

This change will be part of the next release

@wooorm wooorm added the 🙉 open/needs-info This needs some more info label Aug 7, 2021
@jstejada
Copy link

jstejada commented Dec 3, 2021

has this been released yet?

@ChristianMurphy
Copy link
Member

It is ready for release, but hasn't been yet.
/cc @remarkjs/releasers

@wooorm
Copy link
Member

wooorm commented Dec 4, 2021

@ChristianMurphy your last comment said “This change will be part of the next release”. Sounds like there is more work to do?

@ChristianMurphy
Copy link
Member

Sounds like there is more work to do?

There is, cutting a minor release.

@wooorm
Copy link
Member

wooorm commented Dec 29, 2021

Released.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🙉 open/needs-info This needs some more info 🦋 type/enhancement This is great to have
Development

Successfully merging a pull request may close this issue.

6 participants