From 4ef931cdace9a66d4496ab44f65c732256315887 Mon Sep 17 00:00:00 2001 From: Matthew Mallimo Date: Thu, 7 Mar 2024 10:21:06 -0500 Subject: [PATCH 1/6] feat: add required APIs for react streaming --- .../__snapshots__/index.spec.js.snap | 38 ++++++++ .../__tests__/reactStreaming.spec.jsx | 96 +++++++++++++++++++ packages/holocron/src/index.js | 9 ++ packages/holocron/src/reactStreaming.js | 39 ++++++++ 4 files changed, 182 insertions(+) create mode 100644 packages/holocron/__tests__/reactStreaming.spec.jsx create mode 100644 packages/holocron/src/reactStreaming.js diff --git a/packages/holocron/__tests__/__snapshots__/index.spec.js.snap b/packages/holocron/__tests__/__snapshots__/index.spec.js.snap index 8bb1faf..b1fe86b 100644 --- a/packages/holocron/__tests__/__snapshots__/index.spec.js.snap +++ b/packages/holocron/__tests__/__snapshots__/index.spec.js.snap @@ -1,7 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +// eslint-disable-next-line jest/no-large-snapshots -- this one is ok to be a bit large exports[`public API should not have anything removed 1`] = ` { + "ModuleContext": { + "$$typeof": Symbol(react.context), + "Consumer": { + "$$typeof": Symbol(react.context), + "_context": [Circular], + }, + "Provider": { + "$$typeof": Symbol(react.provider), + "_context": [Circular], + }, + "_currentRenderer": null, + "_currentRenderer2": null, + "_currentValue": undefined, + "_currentValue2": undefined, + "_defaultValue": null, + "_globalName": null, + "_threadCount": 0, + }, + "ReactStreamingContext": { + "$$typeof": Symbol(react.context), + "Consumer": { + "$$typeof": Symbol(react.context), + "_context": [Circular], + }, + "Provider": { + "$$typeof": Symbol(react.provider), + "_context": [Circular], + }, + "_currentRenderer": null, + "_currentRenderer2": null, + "_currentValue": {}, + "_currentValue2": {}, + "_defaultValue": null, + "_globalName": null, + "_threadCount": 0, + }, "RenderModule": [Function], "clearModulesUsingExternals": [Function], "composeModules": [Function], @@ -26,5 +63,6 @@ exports[`public API should not have anything removed 1`] = ` "registerModule": [Function], "setModuleMap": [Function], "setRequiredExternalsRegistry": [Function], + "useAsyncModuleData": [Function], } `; diff --git a/packages/holocron/__tests__/reactStreaming.spec.jsx b/packages/holocron/__tests__/reactStreaming.spec.jsx new file mode 100644 index 0000000..669b9eb --- /dev/null +++ b/packages/holocron/__tests__/reactStreaming.spec.jsx @@ -0,0 +1,96 @@ +import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies -- monorepo, this is at the root +import { renderHook } from '@testing-library/react'; +import { useAsyncModuleData, ReactStreamingContext, ModuleContext } from '../src/reactStreaming'; + +describe('reactStreaming', () => { + it('exports ReactStreamingContext', () => { + expect(ReactStreamingContext).toBeDefined(); + }); + + it('exports ModuleContext', () => { + expect(ModuleContext).toBeDefined(); + }); + + describe('useAsyncModuleData', () => { + // eslint-disable-next-line react/display-name, react/prop-types -- test component + const Providers = ({ moduleName, promise, key }) => ({ children }) => ( + // eslint-disable-next-line react/jsx-no-constructed-context-values -- test component + + + {children} + + + ); + it('should throw a promise if the data is not yet resolved', () => { + const key = 'test'; + const moduleName = 'testModule'; + const streamedPromise = new Promise(() => {}); + const { result } = renderHook(() => { + try { + return useAsyncModuleData(key); + } catch (promise) { + return promise; + } + }, { + wrapper: Providers({ moduleName, promise: streamedPromise, key }), + }); + expect(result.current).toBe(streamedPromise); + }); + + it('should return data once the promise is resolved', () => { + const key = 'test'; + const moduleName = 'testModule'; + let resolve; + const streamedPromise = new Promise((res) => { resolve = res; }); + const { result, rerender } = renderHook(() => { + try { + return useAsyncModuleData(key); + } catch (promise) { + return promise; + } + }, { + wrapper: Providers({ moduleName, promise: streamedPromise, key }), + }); + resolve(); + streamedPromise.data = 'testData'; + rerender(); + expect(result.current).toBe('testData'); + }); + + it('should throw an error if the promise is rejected', () => { + const key = 'test'; + const moduleName = 'testModule'; + let reject; + const streamedPromise = new Promise((_, rej) => { reject = rej; }); + const { result, rerender } = renderHook(() => { + try { + return useAsyncModuleData(key); + } catch (error) { + return error; + } + }, { + wrapper: Providers({ moduleName, promise: streamedPromise, key }), + }); + reject(); + streamedPromise.error = 'testError'; + rerender(); + expect(result.current).toBe('testError'); + }); + it('should return undefined if there is no promise', () => { + const key = 'test'; + const moduleName = 'testModule'; + const streamedPromise = undefined; + const { result } = renderHook(() => { + try { + return useAsyncModuleData(key); + } catch (promise) { + return promise; + } + }, { + wrapper: Providers({ moduleName, promise: streamedPromise, key }), + }); + expect(result.current).toBe(undefined); + }); + }); +}); diff --git a/packages/holocron/src/index.js b/packages/holocron/src/index.js index 0f56383..6a31a7a 100644 --- a/packages/holocron/src/index.js +++ b/packages/holocron/src/index.js @@ -42,6 +42,11 @@ import { getRequiredExternalsRegistry, setRequiredExternalsRegistry, } from './externalRegistry'; +import { + ReactStreamingContext, + ModuleContext, + useAsyncModuleData, +} from './reactStreaming'; // Public API export { @@ -69,4 +74,8 @@ export { setRequiredExternalsRegistry, clearModulesUsingExternals, getModulesUsingExternals, + // Streaming + ReactStreamingContext, + ModuleContext, + useAsyncModuleData, }; diff --git a/packages/holocron/src/reactStreaming.js b/packages/holocron/src/reactStreaming.js new file mode 100644 index 0000000..2f3ff34 --- /dev/null +++ b/packages/holocron/src/reactStreaming.js @@ -0,0 +1,39 @@ +import { createContext, useContext } from 'react'; + +const ReactStreamingContext = createContext({}); + +const ModuleContext = createContext(); + +const useAsyncModuleData = (key) => { + const moduleName = useContext(ModuleContext); + const streamContext = useContext(ReactStreamingContext); + const streamedPromise = streamContext[moduleName]?.[key]; + if ( + streamedPromise + && streamedPromise instanceof Promise + && !streamedPromise.data + && !streamedPromise.error + ) { + streamedPromise + .then((data) => { + streamedPromise.data = data; + }) + .catch((error) => { + streamedPromise.error = error; + }); + + throw streamedPromise; + } + if (streamedPromise?.error) { + // The suspense boundary will re-throw this error to be caught by the nearest error boundary + // https://react.dev/reference/react/Suspense#providing-a-fallback-for-server-errors-and-client-only-content + throw streamedPromise.error; + } + + if (streamedPromise?.data) { + return streamedPromise.data; + } + return undefined; +}; + +export { ReactStreamingContext, ModuleContext, useAsyncModuleData }; From 825e1532d3ab4ac90f4b80f26eab137d39a1a490 Mon Sep 17 00:00:00 2001 From: Matthew Mallimo Date: Fri, 8 Mar 2024 08:48:47 -0500 Subject: [PATCH 2/6] feat: enable module context to store more than just the name --- packages/holocron/__tests__/reactStreaming.spec.jsx | 3 ++- packages/holocron/src/reactStreaming.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/holocron/__tests__/reactStreaming.spec.jsx b/packages/holocron/__tests__/reactStreaming.spec.jsx index 669b9eb..ba67f18 100644 --- a/packages/holocron/__tests__/reactStreaming.spec.jsx +++ b/packages/holocron/__tests__/reactStreaming.spec.jsx @@ -17,7 +17,8 @@ describe('reactStreaming', () => { const Providers = ({ moduleName, promise, key }) => ({ children }) => ( // eslint-disable-next-line react/jsx-no-constructed-context-values -- test component - + {/* eslint-disable-next-line react/jsx-no-constructed-context-values -- test component */} + {children} diff --git a/packages/holocron/src/reactStreaming.js b/packages/holocron/src/reactStreaming.js index 2f3ff34..06c9004 100644 --- a/packages/holocron/src/reactStreaming.js +++ b/packages/holocron/src/reactStreaming.js @@ -5,7 +5,7 @@ const ReactStreamingContext = createContext({}); const ModuleContext = createContext(); const useAsyncModuleData = (key) => { - const moduleName = useContext(ModuleContext); + const { moduleName } = useContext(ModuleContext); const streamContext = useContext(ReactStreamingContext); const streamedPromise = streamContext[moduleName]?.[key]; if ( From a3958461ca5df138bd9ce4aab73918e36deff431 Mon Sep 17 00:00:00 2001 From: Matthew Mallimo Date: Tue, 19 Mar 2024 18:45:35 -0400 Subject: [PATCH 3/6] feat(holocron): new streaming API --- .../holocronModule.spec.jsx.snap | 8 + .../__tests__/holocronModule.spec.jsx | 17 ++ packages/holocron/docs/api/README.md | 196 ++++++++++-------- packages/holocron/src/holocronModule.jsx | 10 +- 4 files changed, 145 insertions(+), 86 deletions(-) diff --git a/packages/holocron/__tests__/__snapshots__/holocronModule.spec.jsx.snap b/packages/holocron/__tests__/__snapshots__/holocronModule.spec.jsx.snap index 185fe6c..949e0d1 100644 --- a/packages/holocron/__tests__/__snapshots__/holocronModule.spec.jsx.snap +++ b/packages/holocron/__tests__/__snapshots__/holocronModule.spec.jsx.snap @@ -79,6 +79,14 @@ The 'reducer' will not be added to the Redux Store without a 'name'.", ] `; +exports[`holocronModule should wrap module with ModuleContext 1`] = ` + +
+ test-module +
+
+`; + exports[`holocronModule should wrap module with no arguments 1`] = `
diff --git a/packages/holocron/__tests__/holocronModule.spec.jsx b/packages/holocron/__tests__/holocronModule.spec.jsx index 6c74f51..9620e00 100644 --- a/packages/holocron/__tests__/holocronModule.spec.jsx +++ b/packages/holocron/__tests__/holocronModule.spec.jsx @@ -29,6 +29,8 @@ import holocronModule, { } from '../src/holocronModule'; import { REDUCER_KEY, LOAD_KEY } from '../src/ducks/constants'; +import { ModuleContext } from '../src/reactStreaming'; + const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }); @@ -187,6 +189,21 @@ describe('holocronModule', () => { expect(renderedModule).toMatchSnapshot(); }); + it('should wrap module with ModuleContext', () => { + const MyModuleComponent = holocronModule({ name: 'test-module' })(() => { + const { moduleName } = React.useContext(ModuleContext); + return
{moduleName}
; + }); + const mockStore = createStore( + (state) => state, + fromJS({ modules: { 'mock-module': { key: 'value' } } }) + ); + const renderedModule = render( + + ).asFragment(); + + expect(renderedModule).toMatchSnapshot(); + }); it('should provide the module state as a plain JS prop if a reducer is provided', () => { const reducer = (state) => state; diff --git a/packages/holocron/docs/api/README.md b/packages/holocron/docs/api/README.md index a89214a..315c343 100644 --- a/packages/holocron/docs/api/README.md +++ b/packages/holocron/docs/api/README.md @@ -52,13 +52,13 @@ Creates the [Redux] store with Holocron compatibility. ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `reducer` | `(state, action) => newState` | `true` | The [Redux reducer] for your application | -| `initialState` | `Immutable.Map` | `false` | The initial state of your [Redux] store | -| `enhancer` | `Function` | `false` | A [Redux enhancer] | -| `localsForBuildInitialState` | `Object` | `false` | Value to pass to [vitruvius]'s `buildInitialState` | -| `extraThunkArguments` | `Object` | `false` | Additional arguments to be passed to [Redux thunks] | +| name | type | required | value | +|------------------------------|-------------------------------|----------|-----------------------------------------------------| +| `reducer` | `(state, action) => newState` | `true` | The [Redux reducer] for your application | +| `initialState` | `Immutable.Map` | `false` | The initial state of your [Redux] store | +| `enhancer` | `Function` | `false` | A [Redux enhancer] | +| `localsForBuildInitialState` | `Object` | `false` | Value to pass to [vitruvius]'s `buildInitialState` | +| `extraThunkArguments` | `Object` | `false` | Additional arguments to be passed to [Redux thunks] | ##### Usage @@ -108,10 +108,10 @@ The optional `holocron` object set to the parent React Component inside a Holocr ###### options object -| name | type | default | required | value | -|------------------------|------------|---------|----------|--------------------------------------------------------------------------------------------------------------------------------------| -| `ssr` *☠️ Deprecated* | `Boolean` | falsy | `false` | enable the (deprecated) load function to be called on the server | -| `provideModuleState` | `Boolean` | truthy | `false` | if specified as `false` the module will not be passed `moduleState` as a prop. This will be the default to falsy in future versions. | +| name | type | default | required | value | +|-----------------------|-----------|---------|----------|--------------------------------------------------------------------------------------------------------------------------------------| +| `ssr` *☠️ Deprecated* | `Boolean` | falsy | `false` | enable the (deprecated) load function to be called on the server | +| `provideModuleState` | `Boolean` | truthy | `false` | if specified as `false` the module will not be passed `moduleState` as a prop. This will be the default to falsy in future versions. | #### Usage @@ -161,10 +161,10 @@ export default HelloWorld; The Holocron Module parent React Components will be provided several props automatically. -| prop name | type | value | -|---|---|---| -| `moduleLoadStatus` | `String` | One of `"loading"`, `"loaded"`, or `"error"`, based on the `load` function | -| `moduleState` | `Object` | The state of the registered reducer after [`.toJS()`] has been called on it | +| prop name | type | value | +|--------------------|----------|-----------------------------------------------------------------------------| +| `moduleLoadStatus` | `String` | One of `"loading"`, `"loaded"`, or `"error"`, based on the `load` function | +| `moduleState` | `Object` | The state of the registered reducer after [`.toJS()`] has been called on it | @@ -180,11 +180,11 @@ A React component for rendering a Holocron module. ##### Props -| name | type | required | value | -|---|---|---|---| -| `moduleName` | `PropTypes.string` | `true` | The name of the Holocron module to be rendered | -| `props` | `PropTypes.object` | `false` | Props to pass the rendered Holocron module | -| `children` | `PropTypes.node` | `false` | Childen passed to the rendered Holocron module | +| name | type | required | value | +|--------------|--------------------|----------|------------------------------------------------| +| `moduleName` | `PropTypes.string` | `true` | The name of the Holocron module to be rendered | +| `props` | `PropTypes.object` | `false` | Props to pass the rendered Holocron module | +| `children` | `PropTypes.node` | `false` | Childen passed to the rendered Holocron module | ##### Usage @@ -222,9 +222,9 @@ An action creator that loads Holocron modules and their data. ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `moduleConfigs` | `[{ name, props }]` | `true` | An array of objects containing module names and their props | +| name | type | required | value | +|-----------------|---------------------|----------|-------------------------------------------------------------| +| `moduleConfigs` | `[{ name, props }]` | `true` | An array of objects containing module names and their props | ##### Usage @@ -249,9 +249,9 @@ An action creator that fetches a Holocron module. ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `moduleName` | `String` | `true` | The name of the Holocron module being fetched | +| name | type | required | value | +|--------------|----------|----------|-----------------------------------------------| +| `moduleName` | `String` | `true` | The name of the Holocron module being fetched | ##### Usage @@ -274,16 +274,16 @@ A [higher order component (HOC)] for registering a load function and/or reducer ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `name` | `String` | `true` | The name of your Holocron module | -| `reducer` | `(state, action) => newState` | `false` | The Redux reducer to register when your module is loaded. *Requires a `name`* | -| `load` *☠️ Deprecated* | `(props) => Promise` or `(props) => (dispatch, getState, ...extra) => Promise` | `false` | A deprecated function that fetches data required by your module. Please use `loadModuleData` instead. | -| `loadModuleData` | `({ store, fetchClient, ownProps, module }) => Promise` | `false` | A function that fetches data required by your module | -| `shouldModuleReload` | `(oldProps, newProps) => Boolean` | `false` | A function to determine if your `loadModuleData` and or `load` function should be called again | -| `mergeProps` | `(stateProps, dispatchProps, ownProps) => Object` | `false` | Passed down to Redux connect | -| `mapStateToProps` | `(state, ownProps) => Object` | `false` | Passed down to Redux connect. This enables `shouldModuleReload` to have access to state. | -| `options` | `Object` | `false` | Additional options | +| name | type | required | value | +|------------------------|--------------------------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------------------| +| `name` | `String` | `true` | The name of your Holocron module | +| `reducer` | `(state, action) => newState` | `false` | The Redux reducer to register when your module is loaded. *Requires a `name`* | +| `load` *☠️ Deprecated* | `(props) => Promise` or `(props) => (dispatch, getState, ...extra) => Promise` | `false` | A deprecated function that fetches data required by your module. Please use `loadModuleData` instead. | +| `loadModuleData` | `({ store, fetchClient, ownProps, module }) => Promise` | `false` | A function that fetches data required by your module | +| `shouldModuleReload` | `(oldProps, newProps) => Boolean` | `false` | A function to determine if your `loadModuleData` and or `load` function should be called again | +| `mergeProps` | `(stateProps, dispatchProps, ownProps) => Object` | `false` | Passed down to Redux connect | +| `mapStateToProps` | `(state, ownProps) => Object` | `false` | Passed down to Redux connect. This enables `shouldModuleReload` to have access to state. | +| `options` | `Object` | `false` | Additional options | ##### Usage @@ -320,10 +320,10 @@ export default holocronModule({ Components using this HOC will be provided several props. -| prop name | type | value | -|---|---|---| -| `moduleLoadStatus` | `String` | One of `"loading"`, `"loaded"`, or `"error"`, based on the `load` function | -| `moduleState` | `Object` | The state of the registered reducer after [`.toJS()`] has been called on it | +| prop name | type | value | +|--------------------|----------|-----------------------------------------------------------------------------| +| `moduleLoadStatus` | `String` | One of `"loading"`, `"loaded"`, or `"error"`, based on the `load` function | +| `moduleState` | `Object` | The state of the registered reducer after [`.toJS()`] has been called on it | @@ -341,10 +341,10 @@ Adds a Holocron module to the registry ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `moduleName` | `String` | `true` | The name of your Holocron module | -| `module` | `Function` | `true` | The Holocron module itself (a React component) | +| name | type | required | value | +|--------------|------------|----------|------------------------------------------------| +| `moduleName` | `String` | `true` | The name of your Holocron module | +| `module` | `Function` | `true` | The Holocron module itself (a React component) | @@ -356,10 +356,10 @@ Retrives a Holocron module from the registry ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `moduleName` | `String` | `true` | The name of the Holocron module being requested | -| `altModules` | `Immutable.Map` | `false` | An alternative set of modules to the registry | +| name | type | required | value | +|--------------|-----------------|----------|-------------------------------------------------| +| `moduleName` | `String` | `true` | The name of the Holocron module being requested | +| `altModules` | `Immutable.Map` | `false` | An alternative set of modules to the registry | ##### Usage @@ -420,7 +420,7 @@ Sets the module map ##### Arguments | name | type | required | value | -| -------------- | --------------- | -------- | ---------------------------------------------- | +|----------------|-----------------|----------|------------------------------------------------| | `newModuleMap` | `Immutable.Map` | `true` | The new module map to replace the existing one | ##### Usage @@ -445,10 +445,10 @@ Used to register an external dependency. `registerExternal` takes the following named arguments: | name | type | required | value | -| --------- | -------- | -------- | -------------------------------------------- | +|-----------|----------|----------|----------------------------------------------| | `name` | `String` | `true` | The name of the external being registered | | `version` | `String` | `true` | The version of the external being registered | -| `module` | `any` | `true` | The external to be registered | +| `module` | `any` | `true` | The external to be registered | ##### Usage @@ -472,7 +472,7 @@ Retrieve the external from registry. `getExternal` takes the following named arguments: | name | type | required | value | -| --------- | -------- | -------- | --------------------------- | +|-----------|----------|----------|-----------------------------| | `name` | `String` | `true` | Name of the external wanted | | `version` | `String` | `true` | Version of the external | @@ -488,7 +488,7 @@ Return all the required externals for Holocron module. ##### Arguments | name | type | required | value | -| ------------ | -------- | -------- | ----------------------- | +|--------------|----------|----------|-------------------------| | `moduleName` | `String` | `true` | Name of Holocron module | @@ -537,7 +537,7 @@ Set the contents for the externals registry ##### Arguments | name | type | required | value | -| ------------------ | -------- | -------- | ------------------------------- | +|--------------------|----------|----------|---------------------------------| | `externalRegistry` | `Object` | `true` | Data for the externals registry | ##### Usage @@ -570,9 +570,9 @@ A selector to determine if a Holocron module has been loaded. ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `moduleName` | `String` | `true` | The name of the Holocron module that may be loaded | +| name | type | required | value | +|--------------|----------|----------|----------------------------------------------------| +| `moduleName` | `String` | `true` | The name of the Holocron module that may be loaded | ##### Usage @@ -594,9 +594,9 @@ A selector to determine if a Holocron module failed to load. ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `moduleName` | `String` | `true` | The name of the Holocron module that may have failed to load | +| name | type | required | value | +|--------------|----------|----------|--------------------------------------------------------------| +| `moduleName` | `String` | `true` | The name of the Holocron module that may have failed to load | ##### Usage @@ -618,9 +618,9 @@ A selector to return the error of a Holocron module that failed to load. ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `moduleName` | `String` | `true` | The name of the Holocron module whose load error will be returned | +| name | type | required | value | +|--------------|----------|----------|-------------------------------------------------------------------| +| `moduleName` | `String` | `true` | The name of the Holocron module whose load error will be returned | ##### Usage @@ -642,9 +642,9 @@ A selector to determine if a module is loading. ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `moduleName` | `String` | `true` | The name of the Holocron module that may be loading | +| name | type | required | value | +|--------------|----------|----------|-----------------------------------------------------| +| `moduleName` | `String` | `true` | The name of the Holocron module that may be loading | ##### Usage @@ -666,9 +666,9 @@ A selector to return the promise from a Holocron module being loaded. ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `moduleName` | `String` | `true` | The name of the Holocron module whose loading promise will be returned | +| name | type | required | value | +|--------------|----------|----------|------------------------------------------------------------------------| +| `moduleName` | `String` | `true` | The name of the Holocron module whose loading promise will be returned | ##### Usage @@ -697,13 +697,13 @@ Updates the module registry with a new module map. ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `moduleMap` | `Object` | `true` | The new module map | -| `onModuleLoad` | `Function` | `false` | The function to call on every module that is loaded | -| `batchModulesToUpdate` | `modules => Array` | `false` | A function that returns an array of arrays of batches of modules to load | -| `getModulesToUpdate` | `Function` | `false` | A function that returns an array of which modules should be updated | -| `listRejectedModules` | `Boolean` | `false` | This changes the response shape to be an object containing both `loadedModules` and `rejectedModules` | +| name | type | required | value | +|------------------------|--------------------|----------|-------------------------------------------------------------------------------------------------------| +| `moduleMap` | `Object` | `true` | The new module map | +| `onModuleLoad` | `Function` | `false` | The function to call on every module that is loaded | +| `batchModulesToUpdate` | `modules => Array` | `false` | A function that returns an array of arrays of batches of modules to load | +| `getModulesToUpdate` | `Function` | `false` | A function that returns an array of which modules should be updated | +| `listRejectedModules` | `Boolean` | `false` | This changes the response shape to be an object containing both `loadedModules` and `rejectedModules` | ##### Usage @@ -737,10 +737,10 @@ Compares two module map entries to see if they are equal. This is intended for u ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `firstModuleEntry` | `Object` | `false` | A module map entry | -| `secondModuleEntry` | `Object` | `false` | Another module map entry | +| name | type | required | value | +|---------------------|----------|----------|--------------------------| +| `firstModuleEntry` | `Object` | `false` | A module map entry | +| `secondModuleEntry` | `Object` | `false` | Another module map entry | ##### Usage @@ -771,10 +771,10 @@ This function can allow a browser to refetch a module directly, without requirin ##### Arguments -| name | type | required | value | -|---|---|---|---| -| `moduleName` | `String` | `true` | The name of the module to try to reload | -| `moduleData` | `Object` | `true` | The URLs and integrity values for all the module | +| name | type | required | value | +|--------------|----------|----------|--------------------------------------------------| +| `moduleName` | `String` | `true` | The name of the module to try to reload | +| `moduleData` | `Object` | `true` | The URLs and integrity values for all the module | ##### Usage @@ -800,6 +800,34 @@ export const MyComponent = (props) => { ``` +### Hooks + +#### `useAsyncModuleData` + +Used to retrieve data that is returned by `loadAsyncModuleData`. This hook is intended to be used for SSR Streaming. Any component that uses this hook should be wrapped with a [Suspense Boundary](https://react.dev/reference/react/Suspense). + +##### Arguments + +| name | type | required | value | +|-------|----------|----------|--------------------------------------------------------| +| `key` | `String` | `true` | The key matching the return from `loadAsyncModuleData` | + +##### Usage + +```js +const MyComponent = () => { + const data = useAsyncModuleData('my-module'); + return
{data}
; +}; + +const MyWrapper = () => ( + Loading...
}> + + +); +``` + + [Redux]: https://redux.js.org diff --git a/packages/holocron/src/holocronModule.jsx b/packages/holocron/src/holocronModule.jsx index 4a0b2b9..7060965 100644 --- a/packages/holocron/src/holocronModule.jsx +++ b/packages/holocron/src/holocronModule.jsx @@ -18,6 +18,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import hoistStatics from 'hoist-non-react-statics'; +import { ModuleContext } from './reactStreaming'; import { LOAD_KEY, REDUCER_KEY, @@ -134,8 +135,13 @@ export default function holocronModule({ }; }, []); - // eslint-disable-next-line react/jsx-props-no-spreading -- spread props - return ; + return ( + // eslint-disable-next-line react/jsx-no-constructed-context-values -- name is static + + {/* eslint-disable-next-line react/jsx-props-no-spreading -- props are unknown */} + + + ); }; HolocronModuleWrapper.propTypes = { From feccb305b3e91ed1ef910423297a22790926b8a5 Mon Sep 17 00:00:00 2001 From: Matthew Mallimo Date: Sat, 23 Mar 2024 17:50:32 -0400 Subject: [PATCH 4/6] fix: initiate module context with useMemo --- packages/holocron/src/holocronModule.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/holocron/src/holocronModule.jsx b/packages/holocron/src/holocronModule.jsx index 7060965..78d839c 100644 --- a/packages/holocron/src/holocronModule.jsx +++ b/packages/holocron/src/holocronModule.jsx @@ -12,7 +12,7 @@ * under the License. */ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useMemo } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; @@ -104,6 +104,7 @@ export default function holocronModule({ const isMounted = useRef(false); const loadCountRef = useRef(0); const prevPropsRef = useRef({}); + const moduleContextValue = useMemo(() => ({ moduleName: name }), []); const initiateLoad = (currentLoadCount, frozenProps) => executeLoadingFunctions({ loadModuleData, @@ -136,8 +137,7 @@ export default function holocronModule({ }, []); return ( - // eslint-disable-next-line react/jsx-no-constructed-context-values -- name is static - + {/* eslint-disable-next-line react/jsx-props-no-spreading -- props are unknown */} From 82a8381090d6d835f8417486413c4e036406a4de Mon Sep 17 00:00:00 2001 From: Matthew Mallimo Date: Sat, 23 Mar 2024 17:56:36 -0400 Subject: [PATCH 5/6] test: reformat eslint disables --- packages/holocron/__tests__/reactStreaming.spec.jsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/holocron/__tests__/reactStreaming.spec.jsx b/packages/holocron/__tests__/reactStreaming.spec.jsx index ba67f18..6bca640 100644 --- a/packages/holocron/__tests__/reactStreaming.spec.jsx +++ b/packages/holocron/__tests__/reactStreaming.spec.jsx @@ -13,15 +13,20 @@ describe('reactStreaming', () => { }); describe('useAsyncModuleData', () => { - // eslint-disable-next-line react/display-name, react/prop-types -- test component + /* eslint-disable + react/display-name, + react/prop-types, + react/jsx-no-constructed-context-values -- test component */ const Providers = ({ moduleName, promise, key }) => ({ children }) => ( - // eslint-disable-next-line react/jsx-no-constructed-context-values -- test component - {/* eslint-disable-next-line react/jsx-no-constructed-context-values -- test component */} {children} + /* eslint-enable + react/display-name, + react/prop-types, + react/jsx-no-constructed-context-values -- test component */ ); it('should throw a promise if the data is not yet resolved', () => { const key = 'test'; From 30e41f63daafabf85b9866acf59dfcee55561f9f Mon Sep 17 00:00:00 2001 From: Matthew Mallimo Date: Mon, 25 Mar 2024 12:31:29 -0400 Subject: [PATCH 6/6] refactor: move context holocronModule --- packages/holocron/__tests__/holocronModule.spec.jsx | 3 +-- packages/holocron/__tests__/reactStreaming.spec.jsx | 7 ++----- packages/holocron/src/holocronModule.jsx | 7 +++++-- packages/holocron/src/index.js | 4 ++-- packages/holocron/src/reactStreaming.js | 5 ++--- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/holocron/__tests__/holocronModule.spec.jsx b/packages/holocron/__tests__/holocronModule.spec.jsx index 9620e00..267bf4a 100644 --- a/packages/holocron/__tests__/holocronModule.spec.jsx +++ b/packages/holocron/__tests__/holocronModule.spec.jsx @@ -26,11 +26,10 @@ import holocronModule, { executeLoad, executeLoadModuleData, executeLoadingFunctions, + ModuleContext, } from '../src/holocronModule'; import { REDUCER_KEY, LOAD_KEY } from '../src/ducks/constants'; -import { ModuleContext } from '../src/reactStreaming'; - const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }); diff --git a/packages/holocron/__tests__/reactStreaming.spec.jsx b/packages/holocron/__tests__/reactStreaming.spec.jsx index 6bca640..753a228 100644 --- a/packages/holocron/__tests__/reactStreaming.spec.jsx +++ b/packages/holocron/__tests__/reactStreaming.spec.jsx @@ -1,17 +1,14 @@ import React from 'react'; // eslint-disable-next-line import/no-extraneous-dependencies -- monorepo, this is at the root import { renderHook } from '@testing-library/react'; -import { useAsyncModuleData, ReactStreamingContext, ModuleContext } from '../src/reactStreaming'; +import { useAsyncModuleData, ReactStreamingContext } from '../src/reactStreaming'; +import { ModuleContext } from '../src/holocronModule'; describe('reactStreaming', () => { it('exports ReactStreamingContext', () => { expect(ReactStreamingContext).toBeDefined(); }); - it('exports ModuleContext', () => { - expect(ModuleContext).toBeDefined(); - }); - describe('useAsyncModuleData', () => { /* eslint-disable react/display-name, diff --git a/packages/holocron/src/holocronModule.jsx b/packages/holocron/src/holocronModule.jsx index 78d839c..e37cbd8 100644 --- a/packages/holocron/src/holocronModule.jsx +++ b/packages/holocron/src/holocronModule.jsx @@ -12,13 +12,14 @@ * under the License. */ -import React, { useState, useRef, useMemo } from 'react'; +import React, { + useState, useRef, useMemo, createContext, +} from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import hoistStatics from 'hoist-non-react-statics'; -import { ModuleContext } from './reactStreaming'; import { LOAD_KEY, REDUCER_KEY, @@ -26,6 +27,8 @@ import { INIT_MODULE_STATE, } from './ducks/constants'; +export const ModuleContext = createContext(); + // Execute deprecated load function and provide deprecation message export function executeLoad({ dispatch, load, ...restProps } = {}) { if (load) { diff --git a/packages/holocron/src/index.js b/packages/holocron/src/index.js index 6a31a7a..9eb520a 100644 --- a/packages/holocron/src/index.js +++ b/packages/holocron/src/index.js @@ -33,6 +33,7 @@ import { import { composeModules } from './ducks/compose'; import RenderModule from './RenderModule'; import holocronModule from './publicHolocronModule'; +import { ModuleContext } from './holocronModule'; import forceLoadModule from './loadModule.web'; import { registerExternal, @@ -44,7 +45,6 @@ import { } from './externalRegistry'; import { ReactStreamingContext, - ModuleContext, useAsyncModuleData, } from './reactStreaming'; @@ -65,6 +65,7 @@ export { getLoadingPromise, RenderModule, holocronModule, + ModuleContext, forceLoadModule, registerExternal, getExternal, @@ -76,6 +77,5 @@ export { getModulesUsingExternals, // Streaming ReactStreamingContext, - ModuleContext, useAsyncModuleData, }; diff --git a/packages/holocron/src/reactStreaming.js b/packages/holocron/src/reactStreaming.js index 06c9004..ef778b0 100644 --- a/packages/holocron/src/reactStreaming.js +++ b/packages/holocron/src/reactStreaming.js @@ -1,9 +1,8 @@ import { createContext, useContext } from 'react'; +import { ModuleContext } from './holocronModule'; const ReactStreamingContext = createContext({}); -const ModuleContext = createContext(); - const useAsyncModuleData = (key) => { const { moduleName } = useContext(ModuleContext); const streamContext = useContext(ReactStreamingContext); @@ -36,4 +35,4 @@ const useAsyncModuleData = (key) => { return undefined; }; -export { ReactStreamingContext, ModuleContext, useAsyncModuleData }; +export { ReactStreamingContext, useAsyncModuleData };