-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
RFC: introduce SSRProvider to handle id generation & Portals rendering (
#17938) * Create ssr-context.md * Update ssr-context.md * Update ssr-context.md * Update ssr-context.md * Update ssr-context.md * Update ssr-context.md * Update rfcs/convergence/ssr-context.md Co-authored-by: Oleksandr Fediashov <[email protected]> * prettier Co-authored-by: Oleksandr Fediashov <[email protected]>
- Loading branch information
1 parent
4ea94c9
commit a9eda24
Showing
1 changed file
with
219 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
# 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, and possibly more problems regarding SSR in the future. | ||
|
||
### Autogenerated Ids | ||
|
||
Fluent UI wants autogenerated Ids, because they allow us to make accessibility requirements easier for our consumers by autogenerationg Aria relationshipts (e.g. `aria-describedby`). | ||
|
||
This is not a unique problem, here are issues even in React: | ||
|
||
- [facebook/react#5867](https://github.com/facebook/react/issues/5867) | ||
- [facebook/react#4000](https://github.com/facebook/react/issues/4000) | ||
|
||
The current `useId` prop is not SSR safe because it will increment a constant number as an Id | ||
|
||
```tsx | ||
// Same code run on server and client | ||
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 (probably not though), 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 | ||
|
||
```tsx | ||
// 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. One proposal might be to inline render the tooltip in SSR, but the problem remains the same. Here is the ideal render flow: | ||
|
||
```tsx | ||
/** Null render solution */ | ||
// Server render | ||
<!--Nothing--> | ||
|
||
// First client render | ||
<!--Nothing--> | ||
|
||
// Second client render (after hydration) | ||
<div>tooltip</div> | ||
|
||
/** Render tooltip inline */ | ||
// Server render | ||
<button>Tooltip target</button> | ||
<div>tooltip</div> | ||
|
||
// First client render | ||
<button>Tooltip target</button> | ||
<div>tooltip</div> | ||
|
||
// Second client render (after hydration) | ||
<button>Tooltip target</button> | ||
... | ||
// somewhere on document.body | ||
<div>tooltip</div> | ||
``` | ||
|
||
## 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 after app is hydrated | ||
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 | ||
- Consumers can use our SSR context as a utility but are not bound to it | ||
Cons: | ||
- Extra requirement for consumers that use SSR | ||
- More bundlesize 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. --> |