react-chain-of-responsibility 0.2.1-main.d07f3ea → 0.3.0-main.e6dfb8e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +310 -113
- package/dist/{chunk-LQNXZPUH.mjs → chunk-RAUACXXD.mjs} +30 -26
- package/dist/chunk-RAUACXXD.mjs.map +1 -0
- package/dist/react-chain-of-responsibility.d.mts +14 -4
- package/dist/react-chain-of-responsibility.d.ts +14 -4
- package/dist/react-chain-of-responsibility.fluentUI.js +29 -25
- package/dist/react-chain-of-responsibility.fluentUI.js.map +1 -1
- package/dist/react-chain-of-responsibility.fluentUI.mjs +1 -1
- package/dist/react-chain-of-responsibility.js +29 -25
- package/dist/react-chain-of-responsibility.js.map +1 -1
- package/dist/react-chain-of-responsibility.mjs +1 -1
- package/package.json +34 -20
- package/dist/chunk-LQNXZPUH.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# `react-chain-of-responsibility`
|
|
2
2
|
|
|
3
|
-
[Chain of responsibility design pattern](https://refactoring.guru/design-patterns/chain-of-responsibility) for React component
|
|
3
|
+
[Chain of responsibility design pattern](https://refactoring.guru/design-patterns/chain-of-responsibility) for compositing and customizing React component.
|
|
4
4
|
|
|
5
5
|
## Background
|
|
6
6
|
|
|
7
|
-
This package is designed for React component developers to enable component customization via composition using the [chain of responsibility design pattern](https://refactoring.guru/design-patterns/chain-of-responsibility).
|
|
8
|
-
|
|
9
|
-
Additional entrypoint and hook are provided to use with [Fluent UI as `IRenderFunction`](https://github.com/microsoft/fluentui/blob/master/packages/utilities/src/IRenderFunction.ts).
|
|
7
|
+
This package is designed for React component developers to enable component customization via composition using the [chain of responsibility design pattern](https://refactoring.guru/design-patterns/chain-of-responsibility). This pattern is also used in [Express](https://expressjs.com/) and [Redux](https://redux.js.org/).
|
|
10
8
|
|
|
11
9
|
By composing customizations, they can be decoupled and published separately. App developers could import these published customizations and orchestrate them to their needs. This pattern encourages [separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) and enables economy of customizability.
|
|
12
10
|
|
|
@@ -14,50 +12,243 @@ By composing customizations, they can be decoupled and published separately. App
|
|
|
14
12
|
|
|
15
13
|
Click here for [our live demo](https://compulim.github.io/react-chain-of-responsibility/).
|
|
16
14
|
|
|
17
|
-
## How to use
|
|
15
|
+
## How to use?
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
3 steps to adopt the chain of responsibility pattern.
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
1. [Create a chain](#create-a-chain)
|
|
20
|
+
1. [Register handlers in the chain](#register-handlers-in-the-chain)
|
|
21
|
+
1. [Make a render request](#make-a-render-request)
|
|
22
|
+
|
|
23
|
+
In this sample, we will use chain of responsibility pattern to create a file preview UI which can handle various file types.
|
|
24
|
+
|
|
25
|
+
### Create a chain
|
|
26
|
+
|
|
27
|
+
A chain consists of multiple handlers (a.k.a. middleware) and each would handle rendering requests.
|
|
28
|
+
|
|
29
|
+
The request will be passed to the first handler and may traverse down the chain. The returning result will be a React component. If the chain decided not to render anything, it will return `undefined`.
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
22
32
|
import { createChainOfResponsibility } from 'react-chain-of-responsibility';
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
type Request = { contentType: string };
|
|
35
|
+
type Props = { url: string };
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const Italic = ({ children }) => <i>{children}</i>;
|
|
30
|
-
const Plain = ({ children }) => <>{children}</>;
|
|
37
|
+
const { asMiddleware, Provider, Proxy } = createChainOfResponsibility<Request, Props>();
|
|
38
|
+
```
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
In this sample, the `request` contains file type. And the `props` contains the URL of the file.
|
|
41
|
+
|
|
42
|
+
Tips: `request` is appearance, while `props` is for content.
|
|
43
|
+
|
|
44
|
+
### Register handlers in the chain
|
|
45
|
+
|
|
46
|
+
Based on the rendering request, each middleware is called in turn and they will make decision:
|
|
47
|
+
|
|
48
|
+
- Will render
|
|
49
|
+
- Will render a component on its own
|
|
50
|
+
- Will render a component by compositing component from the next middleware in the chain
|
|
51
|
+
- Will not render
|
|
52
|
+
- Will not render anything at all
|
|
53
|
+
- Will not render, but let the next middleware in the chain to decide what to render
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// Will handle request with content type `image/*`.
|
|
57
|
+
const Image = ({ middleware: { request, Next }, url }) =>
|
|
58
|
+
request.contentType.startsWith('image/') ? (
|
|
59
|
+
<img src={url} />
|
|
60
|
+
) : (
|
|
61
|
+
<Next />
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Will handle request with content type `video/*`.
|
|
65
|
+
const Video = ({ middleware: { request, Next }, url }) =>
|
|
66
|
+
request.contentType.startsWith('video/') ? (
|
|
67
|
+
<video>
|
|
68
|
+
<source src={url} />
|
|
69
|
+
</video>
|
|
70
|
+
) : (
|
|
71
|
+
<Next />
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Will handle everything.
|
|
75
|
+
const Binary = ({ url }) => <a href={url}>{url}</a>;
|
|
76
|
+
|
|
77
|
+
const middleware = [asMiddleware(Image), asMiddleware(Video), asMiddleware(Binary)];
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
In this sample, 3 middleware are registered:
|
|
81
|
+
|
|
82
|
+
- `<Image>` will render `<img>` if content type is `'image/*'`, otherwise, will pass to next middleware (`<Video>`)
|
|
83
|
+
- `<Video>` will render `<video>` if content type is `'video/*'`, otherwise, will pass to next middleware (`<Binary>`)
|
|
84
|
+
- `<Binary>` is a catch-all and will render as a link
|
|
85
|
+
|
|
86
|
+
Notes: props passed to `<Next>` will override original props, however, `request` cannot be overridden.
|
|
38
87
|
|
|
88
|
+
### Make a render request
|
|
89
|
+
|
|
90
|
+
Before calling any components or hooks, the `<Provider>` component must be set up with the chain.
|
|
91
|
+
|
|
92
|
+
When `<Proxy>` is being rendered, it will pass the `request` to the chain. The component returned from the chain will be rendered with `...props`. If no component is returned, it will be rendered as `undefined`.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
39
95
|
render(
|
|
40
96
|
<Provider middleware={middleware}>
|
|
41
|
-
<Proxy request="
|
|
42
|
-
<Proxy request="
|
|
43
|
-
<Proxy
|
|
97
|
+
<Proxy request={{ contentType: 'image/png' }} url="https://.../cat.png" />
|
|
98
|
+
<Proxy request={{ contentType: 'video/mp4' }} url="https://.../cat-jump.mp4" />
|
|
99
|
+
<Proxy request={{ contentType: 'application/octet-stream' }} url="https://.../cat.zip" />
|
|
44
100
|
</Provider>
|
|
45
101
|
);
|
|
46
102
|
```
|
|
47
103
|
|
|
48
|
-
|
|
104
|
+
The code above will render:
|
|
49
105
|
|
|
50
|
-
|
|
106
|
+
```html
|
|
107
|
+
<img src="https://.../cat.png" />
|
|
108
|
+
<video>
|
|
109
|
+
<source src="https://.../cat-jump.mp4" />
|
|
110
|
+
</video>
|
|
111
|
+
<a href="https://.../cat.zip">https://.../cat.zip</a>
|
|
112
|
+
```
|
|
51
113
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
114
|
+
For advanced scenario with precise rendering control, use the `useBuildComponentCallback` hook. This can be found in our live demo and [latter sections](#make-render-request-through-usebuildmiddlewarecallback).
|
|
115
|
+
|
|
116
|
+
## How should I use?
|
|
117
|
+
|
|
118
|
+
Here are some recipes on leveraging the chain of responsibility pattern for UI composition.
|
|
119
|
+
|
|
120
|
+
### Bring your own component
|
|
121
|
+
|
|
122
|
+
Customer of a component library can "bring your own" component by registering their component in the `<Provider>` component.
|
|
123
|
+
|
|
124
|
+
For example, in a date picker UI, using the chain of responsibility pattern enables app developer to bring their own "month picker" component.
|
|
125
|
+
|
|
126
|
+
```diff
|
|
127
|
+
type MonthPickerProps = Readonly<{
|
|
128
|
+
onChange: (date: Date) => void;
|
|
129
|
+
value: Date
|
|
130
|
+
}>;
|
|
131
|
+
|
|
132
|
+
function MonthPicker(props: MonthPickerProps) {
|
|
133
|
+
return (
|
|
134
|
+
<div>
|
|
135
|
+
{props.value.toLocaleDateString(undefined, { month: 'long' })}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
+ const {
|
|
141
|
+
+ asMiddleware: asMonthPickerMiddleware,
|
|
142
|
+
+ Provider: MonthPickerProvider,
|
|
143
|
+
+ Proxy: MonthPickerProxy
|
|
144
|
+
+ } = createChainOfResponsibility<undefined, MonthPickerProps>();
|
|
145
|
+
|
|
146
|
+
type DatePickerProps = Readonly<{
|
|
147
|
+
+ monthPickerComponent?: ComponentType<MonthPickerProps> | undefined;
|
|
148
|
+
onChange: (date: Date) => void;
|
|
149
|
+
value: Date;
|
|
150
|
+
}>;
|
|
151
|
+
|
|
152
|
+
+ const monthPickerMiddleware = asMonthPickerMiddleware(MonthPicker);
|
|
153
|
+
|
|
154
|
+
function DatePicker(props: DatePickerProps) {
|
|
155
|
+
+ const monthPickerMiddleware = useMemo(
|
|
156
|
+
+ () =>
|
|
157
|
+
+ props.monthPickerComponent
|
|
158
|
+
+ ? [
|
|
159
|
+
+ asMonthPickerMiddleware(props.monthPickerComponent),
|
|
160
|
+
+ monthPickerMiddleware
|
|
161
|
+
+ ]
|
|
162
|
+
+ : [monthPickerMiddleware],
|
|
163
|
+
+ [props.monthPickerComponent]
|
|
164
|
+
+ );
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
+ <MonthPickerProvider middleware={monthPickerMiddleware}>
|
|
168
|
+
<div>
|
|
169
|
+
- <MonthPicker onChange={onChange} value={value} />
|
|
170
|
+
+ <MonthPickerProxy onChange={onChange} value={value} />
|
|
171
|
+
<Calendar value={value} />
|
|
172
|
+
</div>
|
|
173
|
+
+ </MonthPickerProvider>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
56
176
|
```
|
|
57
177
|
|
|
58
|
-
###
|
|
178
|
+
### Customizing component
|
|
59
179
|
|
|
60
|
-
The
|
|
180
|
+
The "which component to render" decision in the middleware enables 4 key customization techniques:
|
|
181
|
+
|
|
182
|
+
- Add a new component
|
|
183
|
+
- Register a new `<Audio>` middleware component to handle content type of "audio/\*"
|
|
184
|
+
- Replace an existing component
|
|
185
|
+
- Register a new `<Image2>` middleware component to handle content type of "image/\*"
|
|
186
|
+
- Remove an existing component
|
|
187
|
+
- Return `undefined` when handling content type of "video/\*"
|
|
188
|
+
- Decorate an existing component
|
|
189
|
+
- Return a component which render `<div class="my-border"><Next /></div>`
|
|
190
|
+
|
|
191
|
+
### Improve load time through code splitting and lazy loading
|
|
192
|
+
|
|
193
|
+
After a bundle is lazy-loaded, register the component in the middleware.
|
|
194
|
+
|
|
195
|
+
When the chain of the `<Provider>` is updated, the lazy-loaded component will be rendered immediately.
|
|
196
|
+
|
|
197
|
+
This recipe can also used to build multiple flavors of bundle and allow bundle to be composited to suit the apps need.
|
|
198
|
+
|
|
199
|
+
## Advanced usage
|
|
200
|
+
|
|
201
|
+
### Registering component using functional pattern
|
|
202
|
+
|
|
203
|
+
The `asMiddleware()` is a helper function to turn a React component into a component middleware for simpler registration. As it operates in render-time, there are disadvantages. For example, a VDOM node is always required.
|
|
204
|
+
|
|
205
|
+
If precise rendering control is a requirement, consider registering the component natively using functional programming.
|
|
206
|
+
|
|
207
|
+
The following code snippet shows the conversion from the `<Image>` middleware component in our previous sample, into a component registered via functional programming.
|
|
208
|
+
|
|
209
|
+
```diff
|
|
210
|
+
// Simplify the `<Image>` component by removing `middleware` props.
|
|
211
|
+
- const Image = ({ middleware: { request, Next }, url }) =>
|
|
212
|
+
- request.contentType.startsWith('image/') ? <img src={url} /> : <Next />;
|
|
213
|
+
+ const Image = ({ url }) => <img src={url} />;
|
|
214
|
+
|
|
215
|
+
// Registering the `<Image>` component functionally.
|
|
216
|
+
const middleware = [
|
|
217
|
+
- asMiddleware(Image);
|
|
218
|
+
+ () => next => request =>
|
|
219
|
+
+ request.contentType.startsWith('image/') ? Image : next(request)
|
|
220
|
+
];
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Notes: for performance reason, the return value of the `next(request)` should be a stable value. In the example above, the middleware return `Image`, which is a constant and is stable.
|
|
224
|
+
|
|
225
|
+
### Make render request through `useBuildMiddlewareCallback()`
|
|
226
|
+
|
|
227
|
+
Similar the `asMiddleware`, the `<Proxy>` component is a helper component for easier rendering. It shares similar disadvantages.
|
|
228
|
+
|
|
229
|
+
The following code snippet shows the conversion from the `<Proxy>` component into the `useBuildMiddlewareCallback()` hook.
|
|
230
|
+
|
|
231
|
+
```diff
|
|
232
|
+
function App() {
|
|
233
|
+
// Make a render request (a.k.a. build a component.)
|
|
234
|
+
+ const buildMiddleware = useBuildMiddlewareCallback();
|
|
235
|
+
+ const FilePreview = buildMiddleware({ contextType: 'image/png' });
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<Provider middleware={middleware}>
|
|
239
|
+
{/* Simplify the element by removing `request` props and handling `FilePreview` if it is `undefined`. */}
|
|
240
|
+
- <Proxy request={{ contentType: 'image/png' }} url="https://.../cat.png" />
|
|
241
|
+
+ {FilePreview && <FilePreview url="https://.../cat.png" />}
|
|
242
|
+
</Provider>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Using as `IRenderFunction` in Fluent UI v8
|
|
248
|
+
|
|
249
|
+
> We are considering deprecating the `IRenderFunction` as Fluent UI no longer adopt this pattern.
|
|
250
|
+
|
|
251
|
+
The chain of responsibility design pattern can be used in Fluent UI v8.
|
|
61
252
|
|
|
62
253
|
After calling `createChainOfResponsibilityForFluentUI`, it will return `useBuildRenderFunction` hook. This hook, when called, will return a function to use as [`IRenderFunction`](https://github.com/microsoft/fluentui/blob/master/packages/utilities/src/IRenderFunction.ts) in Fluent UI components.
|
|
63
254
|
|
|
@@ -106,70 +297,19 @@ There are subtle differences between the standard version and the Fluent UI vers
|
|
|
106
297
|
- They are optional too, as defined in [`IRenderFunction`](https://github.com/microsoft/fluentui/blob/master/packages/utilities/src/IRenderFunction.ts)
|
|
107
298
|
- Automatic fallback to `defaultRender`
|
|
108
299
|
|
|
109
|
-
### Decorating UI components
|
|
110
|
-
|
|
111
|
-
One core feature of chain of responsibility design pattern is allowing middleware to control the execution flow. In other words, middleware can decorate the result of their `next()` middleware. The code snippet below shows how the wrapping could be done.
|
|
112
|
-
|
|
113
|
-
The bold middleware uses traditional approach to wrap the `next()` result, which is a component named `<Next>`. The italic middleware uses [`react-wrap-with`](https://npmjs.com/package/react-wrap-with/) to simplify the wrapping code.
|
|
114
|
-
|
|
115
|
-
```jsx
|
|
116
|
-
const middleware = [
|
|
117
|
-
() => next => request => {
|
|
118
|
-
const Next = next(request);
|
|
119
|
-
|
|
120
|
-
if (request?.has('bold')) {
|
|
121
|
-
return props => <Bold>{Next && <Next {...props} />}</Bold>;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return Next;
|
|
125
|
-
},
|
|
126
|
-
() => next => request => wrapWith(request?.has('italic') && Italic)(next(request)),
|
|
127
|
-
() => () => () => Plain
|
|
128
|
-
];
|
|
129
|
-
|
|
130
|
-
const App = () => (
|
|
131
|
-
<Provider middleware={middleware}>
|
|
132
|
-
<Proxy request={new Set(['bold'])}>This is bold.</Proxy>
|
|
133
|
-
<Proxy request={new Set(['italic'])}>This is italic.</Proxy>
|
|
134
|
-
<Proxy request={new Set(['bold', 'italic'])}>This is bold and italic.</Proxy>
|
|
135
|
-
<Proxy>This is plain.</Proxy>
|
|
136
|
-
</Provider>
|
|
137
|
-
);
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
This sample will render:
|
|
141
|
-
|
|
142
|
-
> **This is bold.** _This is italic._ **_This is bold and italic._** This is plain.
|
|
143
|
-
|
|
144
|
-
```jsx
|
|
145
|
-
<Bold>This is bold.</Bold>
|
|
146
|
-
<Italic>This is italic.</Italic>
|
|
147
|
-
<Bold><Italic>This is bold and italic.</Italic></Bold>
|
|
148
|
-
<Plain>This is plain.</Plain>
|
|
149
|
-
```
|
|
150
|
-
|
|
151
300
|
### Nesting `<Provider>`
|
|
152
301
|
|
|
153
302
|
If the `<Provider>` from the same chain appears nested in the tree, the `<Proxy>` will render using the middleware from the closest `<Provider>` and fallback up the chain. The following code snippet will render "Second First".
|
|
154
303
|
|
|
155
304
|
```jsx
|
|
156
|
-
const { Provider, Proxy } = createChainOfResponsibility();
|
|
305
|
+
const { asMiddleware, Provider, Proxy } = createChainOfResponsibility();
|
|
157
306
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return () => <Fragment>First {NextComponent && <NextComponent />}</Fragment>;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const secondMiddleware = () => next => request => {
|
|
165
|
-
const NextComponent = next(request);
|
|
166
|
-
|
|
167
|
-
return () => <Fragment>Second {NextComponent && <NextComponent />}</Fragment>;
|
|
168
|
-
};
|
|
307
|
+
const First = ({ middleware: { Next } }) => <>First <Next /></>;
|
|
308
|
+
const Second = ({ middleware: { Next } }) => <>Second <Next /></>;
|
|
169
309
|
|
|
170
310
|
render(
|
|
171
|
-
<Provider middleware={[
|
|
172
|
-
<Provider middleware={[
|
|
311
|
+
<Provider middleware={[asMiddleware(First)]}>
|
|
312
|
+
<Provider middleware={[asMiddleware(Second)]}>
|
|
173
313
|
<Proxy /> <!-- Renders "Second First" -->
|
|
174
314
|
</Provider>
|
|
175
315
|
</Provider>
|
|
@@ -182,29 +322,41 @@ render(
|
|
|
182
322
|
function createChainOfResponsibility<Request = undefined, Props = { children?: never }, Init = undefined>(
|
|
183
323
|
options?: Options
|
|
184
324
|
): {
|
|
325
|
+
asMiddleware(
|
|
326
|
+
middlewareComponent: ComponentType<MiddlewareComponentProps<Request, Props, Init>>
|
|
327
|
+
): ComponentMiddleware<Request, Props, Init>;
|
|
185
328
|
Provider: ComponentType<ProviderProps<Request, Props, Init>>;
|
|
186
329
|
Proxy: ComponentType<ProxyProps<Request, Props>>;
|
|
187
330
|
types: {
|
|
188
331
|
init: Init;
|
|
189
332
|
middleware: ComponentMiddleware<Request, Props, Init>;
|
|
333
|
+
middlewareComponentProps: MiddlewareComponentProps<Request, Props, Init>;
|
|
190
334
|
props: Props;
|
|
191
335
|
request: Request;
|
|
192
336
|
};
|
|
193
|
-
useBuildComponentCallback: (
|
|
337
|
+
useBuildComponentCallback(): (
|
|
338
|
+
request: Request,
|
|
339
|
+
options?: {
|
|
340
|
+
fallbackComponent?: ComponentType<Props> | undefined
|
|
341
|
+
}
|
|
342
|
+
) => ComponentType<Props> | undefined;
|
|
194
343
|
};
|
|
195
344
|
```
|
|
196
345
|
|
|
197
346
|
### Return value
|
|
198
347
|
|
|
199
|
-
| Name |
|
|
200
|
-
| --------------------------- |
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
348
|
+
| Name | Description |
|
|
349
|
+
| --------------------------- | ------------------------------------------------------------------------------------- |
|
|
350
|
+
| `asMiddleware` | A helper function to convert a React component into a middleware. |
|
|
351
|
+
| `Provider` | Entrypoint component, must wraps all usage of customizations |
|
|
352
|
+
| `Proxy` | Proxy component, process the `request` from props and morph into the result component |
|
|
353
|
+
| `types` | TypeScript: shorthand types, all objects are `undefined` intentionally |
|
|
354
|
+
| `useBuildComponentCallback` | Callback hook which return a function to build the component for rendering the result |
|
|
205
355
|
|
|
206
356
|
### Options
|
|
207
357
|
|
|
358
|
+
> `passModifiedRequest` is not supported by `asMiddleware`.
|
|
359
|
+
|
|
208
360
|
```ts
|
|
209
361
|
type Options = {
|
|
210
362
|
/** Allows a middleware to pass another request object to its next middleware. Default is false. */
|
|
@@ -212,23 +364,44 @@ type Options = {
|
|
|
212
364
|
};
|
|
213
365
|
```
|
|
214
366
|
|
|
215
|
-
If `passModifiedRequest` is default or `false`, middleware will not be allowed to pass another reference of `request` object to their `next()` middleware. Instead, the `request` object passed to `next()` will be ignored and the next middleware always receive the original `request` object. This behavior is similar to [
|
|
367
|
+
If `passModifiedRequest` is default or `false`, middleware will not be allowed to pass another reference of `request` object to their `next()` middleware. Instead, the `request` object passed to `next()` will be ignored and the next middleware always receive the original `request` object. This behavior is similar to [Express](https://expressjs.com/) middleware.
|
|
216
368
|
|
|
217
369
|
Setting to `true` will enable advanced scenarios and allow a middleware to influence their downstreamers.
|
|
218
370
|
|
|
219
371
|
When the option is default or `false`, middleware could still modify the `request` object and influence their downstreamers. It is recommended to follow immutable pattern when handling the `request` object, or use deep [`Object.freeze()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) to guarantee immutability.
|
|
220
372
|
|
|
373
|
+
### API of `asMiddleware`
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
function asMiddleware(
|
|
377
|
+
middlewareComponent: ComponentType<MiddlewareComponentProps<Request, Props, Init>>
|
|
378
|
+
): ComponentMiddleware<Request, Props, Init>;
|
|
379
|
+
|
|
380
|
+
type MiddlewareProps<Request, Props, Init> = Readonly<{
|
|
381
|
+
init: Init;
|
|
382
|
+
Next: ComponentType<Partial<Props>>;
|
|
383
|
+
request: Request;
|
|
384
|
+
}>;
|
|
385
|
+
|
|
386
|
+
type MiddlewareComponentProps<Request, Props, Init> = Props &
|
|
387
|
+
Readonly<{
|
|
388
|
+
middleware: MiddlewareProps<Request, Props, Init>
|
|
389
|
+
}>;
|
|
390
|
+
```
|
|
391
|
+
|
|
221
392
|
### API of `useBuildComponentCallback`
|
|
222
393
|
|
|
223
394
|
```ts
|
|
224
|
-
type UseBuildComponentCallbackOptions<Props> = { fallbackComponent?: ComponentType<Props> |
|
|
395
|
+
type UseBuildComponentCallbackOptions<Props> = { fallbackComponent?: ComponentType<Props> | undefined };
|
|
225
396
|
|
|
226
397
|
type UseBuildComponentCallback<Request, Props> = (
|
|
227
398
|
request: Request,
|
|
228
399
|
options?: UseBuildComponentCallbackOptions<Props>
|
|
229
|
-
) => ComponentType<Props> |
|
|
400
|
+
) => ComponentType<Props> | undefined;
|
|
230
401
|
```
|
|
231
402
|
|
|
403
|
+
For simplicity, instead of returning a component or `false`/`null`/`undefined`, the `useBuildComponentCallback` will only return a component or `undefined`.
|
|
404
|
+
|
|
232
405
|
The `fallbackComponent` is a component which all unhandled requests will sink into, including calls without ancestral `<Provider>`.
|
|
233
406
|
|
|
234
407
|
### API for Fluent UI
|
|
@@ -259,9 +432,24 @@ When rendering the element, `getKey` is called to compute the `key` attribute. T
|
|
|
259
432
|
|
|
260
433
|
## Designs
|
|
261
434
|
|
|
262
|
-
###
|
|
435
|
+
### What is the difference between request, and props?
|
|
436
|
+
|
|
437
|
+
- Request is for *appearance*, while props is for *content*
|
|
438
|
+
- Request is for *deciding which component to render*, while props is for *what to render*
|
|
263
439
|
|
|
264
|
-
|
|
440
|
+
For example:
|
|
441
|
+
|
|
442
|
+
- Button
|
|
443
|
+
- Request: button, link button, push button
|
|
444
|
+
- Props: icon, text
|
|
445
|
+
- File preview
|
|
446
|
+
- Request: image preview, video preview
|
|
447
|
+
- Props: file name, URL
|
|
448
|
+
- Input
|
|
449
|
+
- Request: text input, number input, password input
|
|
450
|
+
- Props: label, value, instruction
|
|
451
|
+
|
|
452
|
+
### Why the type of request and props can be different?
|
|
265
453
|
|
|
266
454
|
This is to support advanced scenarios where props are not ready until all rendering components are built.
|
|
267
455
|
|
|
@@ -302,26 +490,35 @@ This is for supporting multiple providers/proxies under a single app/tree.
|
|
|
302
490
|
|
|
303
491
|
To reduce learning curve and likelihood of bugs, we disabled this feature until developers are more proficient with this package.
|
|
304
492
|
|
|
305
|
-
|
|
493
|
+
With the default settings, if the `request` object passed to `next()` differs from the original `request` object, a reminder will be logged in the console.
|
|
494
|
+
|
|
495
|
+
### Why `request` cannot be modified using `asMiddleware` even `passModifiedRequest` is enabled?
|
|
496
|
+
|
|
497
|
+
Request is for deciding which component to render. It is a build-time value.
|
|
498
|
+
|
|
499
|
+
For component registered using `asMiddleware()`, the `request` prop is a render-time value. A render-time value cannot be used to influence build phase.
|
|
500
|
+
|
|
501
|
+
To modify request, the middleware component must be converted to functional programming.
|
|
306
502
|
|
|
307
503
|
## Behaviors
|
|
308
504
|
|
|
309
505
|
### `<Proxy>` vs. `useBuildComponentCallback`
|
|
310
506
|
|
|
311
|
-
Most of the time, use `<Proxy
|
|
507
|
+
Most of the time, use `<Proxy>` unless precise rendering is needed.
|
|
312
508
|
|
|
313
509
|
Behind the scene, `<Proxy>` call `useBuildComponentCallback()` to build the component it would morph into.
|
|
314
510
|
|
|
315
511
|
You can use the following decision tree to know when to use `<Proxy>` vs. `useBuildComponentCallback`
|
|
316
512
|
|
|
317
|
-
- If you
|
|
318
|
-
- For example, using `useBuildComponentCallback()` allow you to know if the middleware
|
|
513
|
+
- If you need to know what kind of component will be rendered before actual render happen, use `useBuildComponentCallback()`
|
|
514
|
+
- For example, using `useBuildComponentCallback()` allow you to know if the middleware would skip rendering the request
|
|
319
515
|
- If your component use `request` prop which is conflict with `<Proxy>`, use `useBuildComponentCallback()`
|
|
516
|
+
- Also consider using a wrapping component to rename `request` prop
|
|
320
517
|
- Otherwise, use `<Proxy>`
|
|
321
518
|
|
|
322
519
|
### Calling `next()` multiple times
|
|
323
520
|
|
|
324
|
-
It is possible to call `next()` multiple times
|
|
521
|
+
It is possible to call `next()` multiple times. However, the return value should be stable, calling it multiple times without a different request should yield the same result.
|
|
325
522
|
|
|
326
523
|
This is best used with options `passModifiedRequest` set to `true`. This combination allow a middleware to render the UI multiple times with some variations, such as rendering content and minimap at the same time.
|
|
327
524
|
|
|
@@ -335,11 +532,13 @@ This is because React does not allow asynchronous render. An exception will be t
|
|
|
335
532
|
|
|
336
533
|
When writing middleware, keep them as stateless as possible and do not relies on data outside of the `request` object. The way it is writing should be similar to React function components.
|
|
337
534
|
|
|
338
|
-
|
|
535
|
+
When using functional programming to register the middleware, the return value should be stable.
|
|
536
|
+
|
|
537
|
+
### Good middleware returns `false`/`null`/`undefined` to skip rendering
|
|
339
538
|
|
|
340
539
|
If the middleware wants to skip rendering a request, return `false`/`null`/`undefined` directly. Do not return `() => false`, `<Fragment />`, or any other invisible components.
|
|
341
540
|
|
|
342
|
-
This helps the component
|
|
541
|
+
This helps the hosting component to determine whether the request would be rendered or not.
|
|
343
542
|
|
|
344
543
|
### Typing a component which expect no props to be passed
|
|
345
544
|
|
|
@@ -349,27 +548,25 @@ In TypeScript, `{}` literally means any objects. Components of type `ComponentTy
|
|
|
349
548
|
|
|
350
549
|
Although `Record<any, never>` means empty object, it is not extensible. Thus, [`Record<any, never> & { className: string }` means `Record<any, never>`](https://www.typescriptlang.org/play?#code/C4TwDgpgBACgTgezAZygXigJQgYwXAEwB4BDAOxABooyIA3COAPgG4AoUSKAZQFcAjeElQYhKKADIoAbyg4ANiWTIAciQC2EAFxRkwOAEsyAcygBfdmzxk9UMIhQ6+ghyJlzFytZp0BydSRGvubsQA).
|
|
351
550
|
|
|
352
|
-
We believe the best way to type a component which does not allow any props, is `ComponentType<{ children?: undefined }>`.
|
|
353
|
-
|
|
354
551
|
## Inspirations
|
|
355
552
|
|
|
356
553
|
This package is heavily inspired by [Redux](https://redux.js.org/) middleware, especially [`applyMiddleware()`](https://github.com/reduxjs/redux/blob/master/docs/api/applyMiddleware.md) and [`compose()`](https://github.com/reduxjs/redux/blob/master/docs/api/compose.md). We read [this article](https://medium.com/@jacobp100/you-arent-using-redux-middleware-enough-94ffe991e6) to understand the concept, followed by some more readings on functional programming topics.
|
|
357
554
|
|
|
358
555
|
Over multiple years, the chain of responsibility design pattern is proven to be very flexible and extensible in [Bot Framework Web Chat](https://github.co/microsoft/BotFramework-WebChat/). Internal parts of Web Chat is written as middleware consumed by itself. Multiple bundles with various sizes can be offered by removing some middleware and treeshaking them off.
|
|
359
556
|
|
|
360
|
-
Middleware and router in [
|
|
557
|
+
Middleware and router in [Express](https://expressjs.com/) also inspired us to learn more about this pattern. Their fallback middleware always returns 404 is an innovative approach.
|
|
361
558
|
|
|
362
|
-
[
|
|
559
|
+
[Microsoft Copilot](https://copilot.microsoft.com/) helped us understand and experiment with different naming.
|
|
363
560
|
|
|
364
|
-
### Differences from Redux and
|
|
561
|
+
### Differences from Redux and Express
|
|
365
562
|
|
|
366
|
-
The chain of responsibility design pattern implemented in [Redux](https://redux.js.org/) and [
|
|
563
|
+
The chain of responsibility design pattern implemented in [Redux](https://redux.js.org/) and [Express](https://expressjs.com/) prefers fire-and-forget execution (a.k.a. unidirectional): the result from the last middleware will not bubble up back to the first middleware. Instead, the caller may only collect the result from the last middleware, or via asynchronous and intermediate `dispatch()` calls. Sometimes, middleware may interrupt the execution and never return any results.
|
|
367
564
|
|
|
368
|
-
However, the chain of responsibility design pattern implemented in this package prefers call-and-return execution: the result from the last middleware will propagate back to the first middleware before returning to the caller. This gives every middleware a chance to manipulate the result from downstreamers before sending it back.
|
|
565
|
+
However, the chain of responsibility design pattern implemented in this package prefers synchronous call-and-return execution: the result from the last middleware will propagate back to the first middleware before returning to the caller. This gives every middleware a chance to manipulate the result from downstreamers before sending it back.
|
|
369
566
|
|
|
370
567
|
## Plain English
|
|
371
568
|
|
|
372
|
-
This package implements the _chain of responsibility_ design pattern. Based on _request_, the chain of responsibility will be asked to _build a React component_. The middleware
|
|
569
|
+
This package implements the _chain of responsibility_ design pattern. Based on _request_, the chain of responsibility will be asked to _build a React component_. The middleware would _form a chain_ and request is _passed to the first one in the chain_. If the chain decided to render it, a component will be returned, otherwise, `false`/`null`/`undefined`.
|
|
373
570
|
|
|
374
571
|
## Contributions
|
|
375
572
|
|