# RFC: SSR Context @ling1726 @layershifter ## Summary This RFC proposes a specific SSR context will become a requirement for consumers that want to render SSR apps with Fluent. ## Background SSR or Isomorphic apps are first rendered in the server before being delivered to the client. This is generally with code that renders the React app on the server and an additional `hydrate` step on the client where React will attempt to attach event listeners to the existing markup. ```tsx // render app to static HTML on the server ReactDOMServer.renderToString(<App />) res.writeHead( 200, { "Content-Type": "text/html" } ); // On the client const root = document.getElementById( "root" ); ReactDOM.hydrate( <App />, root ); ``` [The React documentation explicitly mentions](https://reactjs.org/docs/react-dom.html#hydrate) that server content and the client's first render to be identical. > React expects that the rendered content is identical between the server and the client. It can patch up differences in text content, but you should treat mismatches as bugs and fix them ## Problem statement The proposal is intended to solve two specific problems that we currently have. ### Autogenerated Ids The current `useId` prop is not SSR safe because it will increment a constant number as an Id ```tsx let id = 0; export function getId() { return id++; } const useId = () => { const ref = React.useRef(); if (ref.current) { ref.current = getId(); } } ``` This *might* work fine intially, but the server will keep that global `id` value growing while on the client it will reset to `0` on every page refresh. React will warn since the server output do not match the client render during hydration ```tsx // server output <div id=1 /> //client output <div id=0 /> ``` ### Portals that are always rendered (Tooltip) Tooltips generally need to be always rendered on the page since they use `aria-describedby` or `aria-labelledby` relationships. These relatioships need to refer to actual DOM elements for screen readers. Tooltips should also be rendered out of order of the DOM to avoid unnecessary overflow and render above page content. > ⚠⚠⚠ document does not exist on the server A naive example that will break during server render ``` // Naive client implementation -> throws const tooltipEl = document.createElement('div'); document.appendChild(tooltipEl); React.createPortal(tooltip, tooltipEl) ``` Let's try to avoid throwing ```tsx // Avoid throwing let toolTipEl; if (typeof document === 'object') { const tooltipEl = document.createElement('div'); document.appendChild(tooltipEl); } if (!tooltipEl) { React.createPortal(tooltip, tooltipEl) } return null; // Server render <!--Nothing--> // Client render <div>tooltip</div> ``` Now the app will successfully render, but React hydration will throw a warning because the server render and client render do not match. ## Detailed Design or Proposal This RFC proposes a new `SSRContext` and `SSRProvider` that needs to wrap around a user SSR app. This will be a necessary contract for SSR apps using `Fluent`. ```tsx <FluentProvider> <SSRProvider> <App /> </SSRProvider> </FluentProvider ``` `SSRProvider` will make all React children aware that they are being used in an SSR context. ### Autogenerated Ids `SSRProvider` will keep the count of ids that is currently being doing with a global value in the the `useId` module as described in examples above. By leveraging [React context default value](https://reactjs.org/docs/context.html#reactcreatecontext) we can also ensure that the same mechanism still works without the `SSRProvider` for client only apps. ```tsx // behaves just like a global `let id = 0` const defaultSSRContext = { current: 0 }; SSRContext = React.createContext(defaultSSRContext); const useId = () => { const context = React.useContext(SSRContext); return React.useMemo(() => ++context.current, [context]); } ``` Nested SSRProviders can just inherit the value for the previous context for consistency in the tree Sibling SSRProviders are a problem. The solution is to seed all SSRProvider ids with sufficiently random value. ### Portal rendering The `Portal` component can be aware of SSR state by consuming context and forcing a rerender after first server render. ```tsx import { defaultContext, useSSRContext } from 'context'; // if the ssrContext is the default value -> we are not in SSR // no probem with first render const [shouldRender, setShouldRender] = React.useState(ssrContextValue === defaultSSRContextValue ); // This if statement technically breaks the rules of hooks, but is safe because the condition never changes after // mounting. if (!isSSR()) { // Force second render once hydration requirement achieved React.useLayoutEffect(() => { if (!shouldRender) { setShouldRender(true); } }, []) ``` ### Pros and Cons Pros: * Autogenerated Ids are safe to use in SSR * Portal will be SSR safe * Same mechanism for other Fluent components to be SSR safe Cons: * Extra requirement for consumers that use SSR <!-- Enumerate the pros and cons of the proposal. Make sure to think about and be clear on the cons or drawbacks of this propsoal. If there are multiple proposals include this for each. --> ## Discarded Solutions <!-- As you enumerate possible solutions, try to keep track of the discarded ones. This should include why we discarded the solution. --> ## Open Issues <!-- Optional section, but useful for first drafts. Use this section to track open issues on unanswered questions regarding the design or proposal. -->