Skip to content

Commit

Permalink
feat(react): allow custom feature app rendering (#557)
Browse files Browse the repository at this point in the history
This deprecates the `renderError` prop.

fixes #296
fixes #295
  • Loading branch information
remmycat committed Feb 19, 2020
1 parent 2ba49e9 commit 03a967a
Show file tree
Hide file tree
Showing 13 changed files with 1,332 additions and 129 deletions.
87 changes: 87 additions & 0 deletions docs/guides/integrating-the-feature-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,15 @@ be provided:
For more details please refer to the the
["Feature App Configs" section](#feature-app-configs).

#### `children`

One can pass a rendering function as the React Children (i.e. the `children`
prop) that allows custom rendering of the Feature App during loading and error
states.

For more details please refer to the the
["Custom Loading and Error UI" section](#custom-loading-and-error-ui).

### React Feature App Container

The `FeatureAppContainer` component allows the integrator to bundle Feature Apps
Expand Down Expand Up @@ -395,6 +404,15 @@ be provided:
For more details please refer to the the
["Feature App Configs" section](#feature-app-configs).

#### `children`

One can pass a rendering function as the React Children (i.e. the `children`
prop) that allows custom rendering of the Feature App during loading and error
states.

For more details please refer to the the
["Custom Loading and Error UI" section](#custom-loading-and-error-ui).

### Error Handling

When a Feature App throws an error while rendering or, in the case of a
Expand All @@ -403,6 +421,63 @@ When a Feature App throws an error while rendering or, in the case of a
however, rendering errors are not caught and must therefore be handled by the
integrator.

### Custom Loading and Error UI

An integrator can customize the rendering of Loading states and Errors, using
the `children` "render prop" in `FeatureAppContainer` or `FeatureAppLoader`.

The `children` prop is a function that receives parameters in form of an object
and returns rendered React children (a React Node).

Please [look at the API reference][custom-rendering-param-api], to learn more
about the passed params:

- [`error`][custom-rendering-param-error-api]
- [`loading`][custom-rendering-param-loading-api]
- [`featureAppNode`][custom-rendering-param-featureappnode-api]

> This API allows full control over the rendering output, and must therefore
> abide a set of rules that need to be followed carefully:
>
> - **The `featureAppNode` might be passed and has to be rendered, even when
> `loading=true`.**
> A Feature App might depend on being rendered, before resolving its loading
> promise. To not show the Feature App in favour of a loading UI, it must be
> **hidden visually** (e.g. via `display: none;`).
> - **The `featureAppNode` should always be rendered into the same position of
> the returned tree.**
> Otherwise it could occur, that React re-mounts the Feature App, which is
> resource-expensive and can break DOM Feature Apps.
#### Custom UI Example

> The following example can also be seen with more context in the ["React
> Loading UI"][react-loading-ui-demo] and ["React Error
> Handling"][react-error-handling-demo] demos.
```jsx
<FeatureAppContainer
featureAppId="some-feature-app"
featureAppDefinition={someFeatureAppDefinition}
//...
>
{({ error, loading, featureAppNode }) => {
if (error) {
return <ErrorUi error={error}>
}

return (
<div>
<div style={{display: loading ? 'none' : 'initial'}}>
{featureAppNode}
</div>
{loading && <Spinner />}
</div>
);
}}
</FeatureAppContainer>
```

## Placing Feature Apps on a Web Page Using Web Components

An integrator can use the `feature-app-loader` or the `feature-app-container`
Expand Down Expand Up @@ -743,3 +818,15 @@ someFeatureService2.foo(42);
[own-feature-service-definitions]:
/docs/guides/writing-a-feature-app#ownfeatureservicedefinitions
[sharing-npm-dependencies]: /docs/guides/sharing-npm-dependencies
[custom-rendering-param-api]:
/@feature-hub/interfaces/react.customfeatureapprenderingparams.html
[custom-rendering-param-error-api]:
/@feature-hub/interfaces/react.customfeatureapprenderingparams.html#error
[custom-rendering-param-loading-api]:
/@feature-hub/interfaces/react.customfeatureapprenderingparams.html#loading
[custom-rendering-param-featureappnode-api]:
/@feature-hub/interfaces/react.customfeatureapprenderingparams.html#featureappnode
[react-loading-ui-demo]:
https://github.com/sinnerschrader/feature-hub/tree/master/packages/demos/src/react-loading-ui
[react-error-handling-demo]:
https://github.com/sinnerschrader/feature-hub/tree/master/packages/demos/src/react-error-handling
45 changes: 45 additions & 0 deletions docs/guides/writing-a-feature-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,48 @@ const myFeatureAppDefinition = {
};
```

### Loading UIs provided by the React Integrator

Both kinds of Feature Apps can specify a loading stage for Feature Apps, which
are used to allow an Integrator to hide an already rendered Feature App visually
and display a custom loading UI instead. This feature is for client-side
rendering only.

A Feature App can declare this loading stage by passing a `Promise` in the
object returned from their `create` function with the key `loadingPromise`. Once
the promise resolves, the loading is considered done. If it rejects, the Feature
App will be considered as crashed, and the Integrator can use the rejection
payload to display a custom Error UI.

```js
const myFeatureAppDefinition = {
create(env) {
const dataPromise = fetchDataFromAPI();

return {
loadingPromise: dataPromise,

render() {
return <App dataPromise={dataPromise}>;
}
};
}
};
```

> **Note:**
> If you want the rendered App to control when it is done loading, you can pass
> the promise `resolve` and `reject` functions into the App using your render
> method. An example for this is implemented in the [`react-loading-ui`
> demo][demo-react-loading-ui].
> **Note:**
> If a similar loading stage (after rendering started) is needed for server-side
> rendering, for example to wait for a data layer like a router to resolve all
> dependencies, it can be implemented using the
> [`@feature-hub/async-ssr-manager`][async-ssr-manager-api]'s `scheduleRerender`
> API.
## Implementing a Feature App for an Integrator That Uses Web Components

If the targeted integrator is using the [`@feature-hub/dom`][dom-api] package, a
Expand Down Expand Up @@ -272,3 +314,6 @@ const myFeatureAppDefinition = {
[issue-245]: https://github.com/sinnerschrader/feature-hub/issues/245
[providing-a-versioned-api]:
/docs/guides/writing-a-feature-service#providing-a-versioned-api
[async-ssr-manager-api]: /@feature-hub/modules/async_ssr_manager.html
[demo-react-loading-ui]:
https://github.com/sinnerschrader/feature-hub/tree/master/packages/demos/src/react-loading-ui
9 changes: 0 additions & 9 deletions packages/demos/src/react-error-handling/feature-app.ts

This file was deleted.

31 changes: 0 additions & 31 deletions packages/demos/src/react-error-handling/index.test.ts

This file was deleted.

34 changes: 0 additions & 34 deletions packages/demos/src/react-error-handling/integrator.tsx

This file was deleted.

69 changes: 69 additions & 0 deletions packages/demos/src/react-loading-and-error-ui/feature-app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {Text} from '@blueprintjs/core';
import {FeatureAppDefinition} from '@feature-hub/core';
import {ReactFeatureApp} from '@feature-hub/react';
import * as React from 'react';

const getFakeServerLag = async (ms: number) =>
new Promise(resolve => setTimeout(resolve, ms));

async function fetchDataFromServer(): Promise<string> {
await getFakeServerLag(3000);

if (Math.random() > 0.5) {
throw Error(`Server Error: Bad luck!
For performance reasons this server flips a coin to consider which calls to answer.
Reload the demo to try again.`);
}

return 'You got lucky! Have some juicy server data. Reload the example for a chance at an error.';
}

interface AppProps {
loadingDone: () => void;
loadingError: (err: Error) => void;
}

const App: React.FunctionComponent<AppProps> = ({
loadingDone,
loadingError
}) => {
const [serverResponse, setServerResponse] = React.useState<string | null>(
null
);

React.useEffect(() => {
fetchDataFromServer()
.then(setServerResponse)
.then(loadingDone)
.catch(loadingError);
}, [loadingDone, loadingError]);

if (!serverResponse) {
return null;
}

return (
<Text>
<span>The feature app is ready to display, after server responded:</span>
<pre>{serverResponse}</pre>
</Text>
);
};

const featureAppDefinition: FeatureAppDefinition<ReactFeatureApp> = {
create: () => {
let resolve: () => void;
let reject: (err: Error) => void;

const loadingPromise: Promise<void> = new Promise(
(res, rej) => ([resolve, reject] = [res, rej])
);

return {
loadingPromise,
render: () => <App loadingDone={resolve} loadingError={reject} />
};
}
};

export default featureAppDefinition;
56 changes: 56 additions & 0 deletions packages/demos/src/react-loading-and-error-ui/integrator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {Callout, Card, H4, Intent, Spinner} from '@blueprintjs/core';
import {createFeatureHub} from '@feature-hub/core';
import {defineExternals, loadAmdModule} from '@feature-hub/module-loader-amd';
import {FeatureAppLoader, FeatureHubContextProvider} from '@feature-hub/react';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import '../blueprint-css';

defineExternals({react: React});

const {featureAppManager} = createFeatureHub('test:integrator', {
moduleLoader: loadAmdModule,
providedExternals: {react: process.env.REACT_VERSION as string}
});

function ErrorUi({error}: {error: Error}): JSX.Element {
return (
<Callout intent={Intent.DANGER}>
<H4>Example Error UI</H4>
<p>{error.message}</p>
</Callout>
);
}

ReactDOM.render(
<div style={{padding: '20px'}}>
<FeatureHubContextProvider value={{featureAppManager}}>
<FeatureAppLoader
featureAppId="test:invalid"
src="feature-app.umd.js"
onError={console.warn}
>
{({error, featureAppNode, loading}) => {
if (error) {
return <ErrorUi error={error} />;
}

return (
<Card>
<div style={{display: loading ? 'none' : 'initial'}}>
{featureAppNode}
</div>
{loading && (
<div>
<Spinner />
Example Loading UI…
</div>
)}
</Card>
);
}}
</FeatureAppLoader>
</FeatureHubContextProvider>
</div>,
document.querySelector('main')
);
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const webpackBaseConfig = require('../webpack-base-config');
*/
const configs = [
merge.smart(webpackBaseConfig, {
entry: path.join(__dirname, './feature-app.ts'),
entry: path.join(__dirname, './feature-app.tsx'),
externals: {
react: 'react'
},
Expand Down
Loading

0 comments on commit 03a967a

Please sign in to comment.