Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loading UI #557

Merged
merged 1 commit into from
Feb 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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