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