diff --git a/text/0000-ssr-cache.md b/text/0000-ssr-cache.md new file mode 100644 index 00000000..e4f18454 --- /dev/null +++ b/text/0000-ssr-cache.md @@ -0,0 +1,219 @@ +- Start Date: 2018-01-23 +- RFC PR: (leave this empty) +- React Issue: (leave this empty) + +# Summary + +Add hooks to ReactDOMServer to support caching + +[See the original discussion here](https://github.com/facebook/react/issues/11670) + +# Basic example + +```js +import { renderToString } from 'react-dom/server'; + +// No cache strategies +renderToString(); + +// Using cache strategy +renderToString(, { cacheStrategy: new ExampleCacheStrategy() }); +``` + +# Motivation + +Why are we doing this? What use cases does it support? What is the expected +outcome? + +* Improve SSR render performance + +# Detailed design + +##### API design goals: + +1. SSR caching is performed at the `frame` level (of the ReactPartialRenderer), where each frame represents a react element + * only complete/closed frames can be cached (this greatly simplifies the SSR cache API) + +2. SSR caching is performed by a `CacheStrategy` implementation + * cache strategies are passed as `options` to `renderToString()` and `renderToStream()` + +3. Cache strategies should be capable using either react components or configuration to enable caching. + + +4. Beyond supporting simple per-component caching, a cache strategy _should be able to support_ `templates`, where a component can be rendered to become a (cached) template, and templates can then have content injected - this has the potential to _drastically_ improve SSR performance. + + +### CacheStrategy API + +```js + +interface CacheStrategy { + + /** + * Gets the cache strategy state for a component. + * + * ReactPartialRenderer hook: resolveElement (called during every resolveElement invocation) + * + * @param component + * @param props + * @param context + * @returns {*} if undefined is returned, the cache strategy render method will not be invoked for this component. + */ + getCacheState(component: ReactNode, props: Object, context: Object): T | undefined, + + /** + * Renders an element using a cache strategy. + * + * ReactPartialRenderer hook: renderFrame (called when rendering a frame that has assigned 'cacheState') + * + * @param element to render + * @param context to use for rendering + * @param cacheState the state returned by the getCacheState() method + * @param renderUtils to simplify rendering of cached component + * @returns {string} the rendered component + */ + render(element: ReactElement, context: Object, cacheState: T, renderUtils: CacheRenderUtils): string, +} + +``` + +#### Cache Strategy Notes + +##### #getCacheState() + * Determines if a component supports caching, and returns component specific cache state + * **Hook** must be called by the `ReactPartialRenderer` every time an element is resolved + * Returning `undefined` indicates that the component does **not** support caching + + +##### #render() + * handles rendering for a component that supports caching + * **Hook** must be called by `ReactPartialRenderer` when rendering a frame that has `cacheState` + * receives the `cacheState` returned by `getCacheState()` + * receives `renderUtils`, utility methods for rendering cached components that abstracts renderer internals + +### CacheRendererUtils + +```js + +// Utility methods for rendering that abstract renderer internals +type CacheRenderUtils = { + + /** + * Renders the current frame element and all its children, allowing props to be overridden. + * + * @param props + * @param context + * @returns {string} the rendered element output + */ + renderCurrentElement: (props?: Object, context?: Object) => string, + + /** + * Renders the provided element and all its children. + * + * @param element + * @param context + * @param domNamespace + * @returns {string} the rendered element output + */ + renderElement: (element: ReactElement, context?: Object, domNamespace?: string) => string, + + /** + * Logs a warning if the base context is modified during the provided render function. + * + * NOTE: This only logs warning messages in development. + * + * @param baseContext the expected context throughout the entire render method + * @param render method + * @param [messageSuffix] {string} the message to log + * @returns {string} the render output + */ + warnIfRenderModifiesContext: (baseContext: Object, render: (ctx: Object) => string, messageSuffix?: string) => string, +}; +``` + +#### Render Util Notes + +##### #renderCurrentElement() + * renders the current element, allowing props and context to be modified + +##### #renderElement() + * renders the provided element + * allows arbitrary elements to be rendered by cache strategies + * this enables cache strategies to be created that supports injecting element(s) into cached content (aka, a template) + * this method can be used to render the element(s) that will be injected into a template + +##### #warnIfRenderModifiesContext() + * enables a cache strategy to determine if the `context` is modified while rendering an element (and all its children) + * this is useful for a cache strategy that supports injecting element(s) into cached content (aka, a template) + * if the context is changed when rendering a template, injected elements may not render consistently + on the server and client + * this method can be used to wrap `renderCurrentElement` or `renderElement` and log a warning if rendering on server/client are at risk of being inconsistent. + + +I've created a [proof of concept you can view in this forked branch](https://github.com/adam-26/react/commits/ssr-hooks). It includes an example `CacheStrategy` implementation and basic app in the [SSR plugins fixture](https://github.com/adam-26/react/tree/ssr-hooks/fixtures/ssrPlugins). + +The proof of concept app includes 3 approaches to caching: +1. Using a `static getCacheKey = (props) => 'key';` method on any component +2. Using a `` component +3. Using a `` component to create templates from components that support injecting content into the cached template. + + +### Template Implementation Example + +IMPORTANT: The `Template` is an example of a hook implementation, it is **NOT** part of the SSR cache feature. + +```js +