From 305ba0c3210fe12c1f55517f1a1e7f17123ba4e2 Mon Sep 17 00:00:00 2001 From: l3ops Date: Sun, 11 Nov 2018 14:58:11 +0100 Subject: [PATCH 1/2] "Serializable data hook" RFC --- text/0000-serializable-data-hook.md | 197 ++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 text/0000-serializable-data-hook.md diff --git a/text/0000-serializable-data-hook.md b/text/0000-serializable-data-hook.md new file mode 100644 index 00000000..efd8d901 --- /dev/null +++ b/text/0000-serializable-data-hook.md @@ -0,0 +1,197 @@ +- Start Date: 2018-11-11 +- RFC PR: (leave this empty) +- React Issue: (leave this empty) + +# Summary + +Introduce a new hook to allow extraction of server-side data and +subsequent client-side hydration along the component tree. + +# Basic example + +```js +// Usage with react-cache +function ResourceProvider({ children, fetch, hash }) { + const resource = useSerializable( + // Create a "resource" object acting as a cache + // and manager for data requests + // + // The `data` argument is optional and may + // contain data serialized by the server + data => createResource(fetch, hash, data), + // Collect the serializable data from the + // resource object + resource => resource.getSerializable(), + ); + + // The resource object is returned from the hook function + // and can be used as a context to fetch data + return ( + + {children} + + ); +} + +// Usage with Redux +function ReduxProvider({ children, reducer }) { + const store = useSerializable( + data => createStore(reducer, data), + store => store.getState(), + ); + + return ( + + {children} + + ); +} + +// Usage with Apollo +function ApolloProvider({ children, config }) { + const client = useSerializable( + data => { + const cache = config.cache || new InMemoryCache(); + if(data) { + cache.restore(data); + } + + return new ApolloClient({ ...config, cache }); + }, + client => client.cache.extract(), + ); + + return ( + + {children} + + ); +} +``` + +# Motivation + +While the Suspense API provides a clear way to handle data fetching in +a React tree, there is currently no way to extract and serialize said +data alongside a server-rendered tree (this is probably outside of the +scope of Suspense, and right now Suspense is not supported on the +server anyway). + +Currently the most commonly used solution to handle both loading and +serializing server-side data is next.js' `getInitialProps` method. But +this solution relies on HOC, and as such suffers from the same +problems (proliferation of components in the tree, potential prop name +collisions). Finally, it doesn't integrate well with Suspense (since +the data fetching is expected to happen in `getInitialProps` before +rendering), especially with code-splitting and `React.lazy` since +`getInitialProps` is a static method that is called before rendering +the component. + +This RFC proposes a completely independent solution from Suspense to +allow declaring a "serializable" data store during a component's +render phase, after it has potentially been lazy-loaded. This would +allow ReactDOMServer to extract this data after rendering the tree, +and ReactDOM to provide it back to the component during the hydration +phase. This essentially hands the concern of handling server-side data +to React, just like the Hooks proposal did with the concern of +handling state and side-effects. + +# Detailed design + +Introduce a new Hook function with the following signature: + +```js +function useSerializable(createResource: (data?: D) => R, collectData: (resource: R) => D): R; +``` + +The `createResource` function is called synchronously when the +component is mounted, and the resulting resource object is stored in +the hook state chain and returned (this part is essentially doing +`useMemo(createResource, [])`). + +The `collectData` function is called at the very end of the render +when all the suspended components have resolved, with the resource +object as its argument, and should return a serializable object. + +Since as mentionned earlier there is currently no support for Suspense +server-side, how the serialized data are actually collected is not +currently part of this RFC. Tentatively, the following API could be +proposed: + +```js +const [html, data] = await renderToStringAsync(); +``` + +Where `html` contains the rendered tree in string form, and `data` +contains an *arbitratry* serializable object, which could then be +provided as an additional argument to the `hydrate` function: + +```js +hydrate(, container, callback, data); +``` + +When mounting the tree, React will now call `createResource` with the +corresponding fragment of data. It's important to note that aside from +being serializable (can be passed to `JSON.stringify` without error), +the format of the `data` object is completely unspecified and should +be handled as an opaque data type. This allows the React +implementation to layout the data as it see fit (though the format +should probably remain stable in a minor release, even if different +client- and server-side versions of ReactDOM should be rare). + +# Drawbacks + +This RFC extends the API surface of React by pushing an additional +concern into the core library. Internally, this may increase the +complexity of both the server-side and client-side renderers in order +to keep track of where the data should be injected. + +In theory this API could be implemented in userland, with one major +difference: manual keys. Since an external library cannot make any +assumption about the React rendering process, `useSerializable` needs +an additional `key: number | string` parameter to match server data +with client components, and just like the `getInitialProps` / HOC +approach this is potentially open to name collisions. + +Finally, this ia smaller nitpick but I don't see a reliable way to run +dead code elimination on the `collectData` parameter to ensure that +this code gets removed in client-side builds. + +# Alternatives + +- Keep server data outside of React core (potentially falling back to the aforementioned userland implementation) +- Use another name instead of `useSerializable` +- Static `createContext` and `collectData` methods for class components + +# Adoption strategy + +While not a breaking change for React itself, this API could become +one for various SSR Frameworks. But due to the way `React.lazy` works +releasing Suspense for the servers will be breaking anyway, and this +API could become a small part of that bigger release. + +# How we teach this + +Due to the cross-environment communication required to pass the data +emitted by the server renderer back to the client renderer, aside from +adding documentation for the new hooks and the `ReactDOMServer` and +`ReactDOM` methods, a new page in the `Advanced Guides` of the +documentation could be added to cover all the aspects of server-side +rendering, including this one. + +Ideally though, for most users the details of the serialization and +hydration would be abstracted away by a framework, leaving the +`useSerializable` hook (or a custom hook based on it) as the only part +of the API effectively exposed to the end user. Since this would be a +core API some libraries may even afford to have the call to +`useSerializable` as part of their Provider component, meaning the end +user gets server data serialization "for free" complexity-wise. + +# Unresolved questions + +Should a "reverse" API be exposed (`renderToString` with an existing +data payload, and calling `collectData` on a mounted root) ? +This would allow some advanced caching scenarios on both the client +and the server, especially for offline use in conjunction with shared +workers. But just like the actual API to collect or hydrate data this +is probably outside of the scope of this RFC. From 88cac52351c24e6ab87cb98dc7dd877909056ee3 Mon Sep 17 00:00:00 2001 From: l3ops Date: Mon, 12 Nov 2018 09:35:59 +0100 Subject: [PATCH 2/2] More infos on recreating the resource --- text/0000-serializable-data-hook.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/text/0000-serializable-data-hook.md b/text/0000-serializable-data-hook.md index efd8d901..2e4f4ea3 100644 --- a/text/0000-serializable-data-hook.md +++ b/text/0000-serializable-data-hook.md @@ -139,6 +139,11 @@ implementation to layout the data as it see fit (though the format should probably remain stable in a minor release, even if different client- and server-side versions of ReactDOM should be rare). +If `hydrate` is called multiple times with a different `data` bundle +(something that should probably not be done anyway as it's bound to +generate a bunch of warning), a warning should be emitted since this +is not supported (the resource will not be recreated). + # Drawbacks This RFC extends the API surface of React by pushing an additional @@ -161,7 +166,8 @@ this code gets removed in client-side builds. - Keep server data outside of React core (potentially falling back to the aforementioned userland implementation) - Use another name instead of `useSerializable` -- Static `createContext` and `collectData` methods for class components +- Static `createResource` and `collectData` methods for class components +- Have the `createResource` function return a tuple of the resource and the `collectData` function (`[R, () => D]`) intead of using two arguments # Adoption strategy @@ -195,3 +201,8 @@ This would allow some advanced caching scenarios on both the client and the server, especially for offline use in conjunction with shared workers. But just like the actual API to collect or hydrate data this is probably outside of the scope of this RFC. + +Should the resource be memoized using the `createResource` function, or +an additional inputs array ? If so, then should the new resource be +recreated using the old data +(`const newResource = createResource(collectData(oldResource));`) ?