Skip to content

Commit 6bcfe2f

Browse files
authored
Add nested provider of same chain (#64)
* Add nested provider of same chain * Propagate enhancer * Add test for fallbackComponent * Update PR number for nested provider * Update variable name * Clean up * Move section * Verbiage * Update useBuildComponentCallback signature
1 parent 28d9c33 commit 6bcfe2f

File tree

4 files changed

+173
-21
lines changed

4 files changed

+173
-21
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- `import { createChainOfResponsibilityForFluentUI } from 'react-chain-of-responsibility/fluentUI'` for Fluent UI renderer function
1515
- Moved build tools from Babel to tsup/esbuild
1616

17+
### Added
18+
19+
- Support nested provider of same type, by [@compulim](https://github.com/compulim) in PR [#64](https://github.com/compulim/react-chain-of-responsibility/pull/64)
20+
- Components will be built using middleware from `<Provider>` closer to the `<Proxy>` and fallback to those farther away
21+
1722
### Changed
1823

1924
- Bumped dependencies, by [@compulim](https://github.com/compulim), in PR [#49](https://github.com/compulim/react-chain-of-responsibility/pull/49), [#58](https://github.com/compulim/react-chain-of-responsibility/pull/58), and [#63](https://github.com/compulim/react-chain-of-responsibility/pull/63)

README.md

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ const Plain = ({ children }) => <>{children}</>;
3131

3232
// Constructs an array of middleware to handle the request and return corresponding subcomponents.
3333
const middleware = [
34-
() => next => request => request === 'bold' ? Bold : next(request),
35-
() => next => request => request === 'italic' ? Italic : next(request),
34+
() => next => request => (request === 'bold' ? Bold : next(request)),
35+
() => next => request => (request === 'italic' ? Italic : next(request)),
3636
() => () => () => Plain
3737
];
3838

@@ -49,6 +49,12 @@ This sample will render:
4949

5050
> **This is bold.** _This is italic._ This is plain.
5151
52+
```jsx
53+
<strong>This is bold.</strong>
54+
<i>This is italic.</i>
55+
<>This is plain.</>
56+
```
57+
5258
### Using with Fluent UI as `IRenderFunction`
5359

5460
The chain of responsibility design pattern can be used in Fluent UI.
@@ -69,8 +75,8 @@ const Orange = () => <>🍊</>;
6975

7076
// Constructs an array of middleware to handle the request and return corresponding subcomponents.
7177
const middleware = [
72-
() => next => props => props?.iconProps?.iconName === 'Banana' ? Banana : next(props),
73-
() => next => props => props?.iconProps?.iconName === 'Orange' ? Orange : next(props)
78+
() => next => props => (props?.iconProps?.iconName === 'Banana' ? Banana : next(props)),
79+
() => next => props => (props?.iconProps?.iconName === 'Orange' ? Orange : next(props))
7480
// Fallback to `defaultRender` of `IRenderFunction` is automatically injected.
7581
];
7682

@@ -135,6 +141,41 @@ This sample will render:
135141
136142
> **This is bold.** _This is italic._ **_This is bold and italic._** This is plain.
137143
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+
### Nesting `<Provider>`
152+
153+
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+
155+
```jsx
156+
const { Provider, Proxy } = createChainOfResponsibility();
157+
158+
const firstMiddleware = () => next => request => {
159+
const NextComponent = next(request);
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+
};
169+
170+
render(
171+
<Provider middleware={[firstMiddleware]}>
172+
<Provider middleware={[secondMiddleware]}>
173+
<Proxy /> <!-- Renders "Second First" -->
174+
</Provider>
175+
</Provider>
176+
);
177+
```
178+
138179
## API
139180
140181
```ts
@@ -155,12 +196,12 @@ function createChainOfResponsibility<Request = undefined, Props = { children?: n
155196
156197
### Return value
157198
158-
| Name | Type | Description |
159-
| --------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------- |
160-
| `Provider` | `React.ComponentType` | Entrypoint component, must wraps all usage of customizations |
161-
| `Proxy` | `React.ComponentType` | Proxy component, process the `request` from props and morph into the result component |
162-
| `types` | `{ init, middleware, props, request }` | TypeScript: shorthand types, all objects are `undefined` intentionally |
163-
| `useBuildComponentCallback` | `() => (request, options) => React.ComponentType` | Callback hook which return a function to build the component for rendering the result |
199+
| Name | Type | Description |
200+
| --------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
201+
| `Provider` | `React.ComponentType` | Entrypoint component, must wraps all usage of customizations |
202+
| `Proxy` | `React.ComponentType` | Proxy component, process the `request` from props and morph into the result component |
203+
| `types` | `{ init, middleware, props, request }` | TypeScript: shorthand types, all objects are `undefined` intentionally |
204+
| `useBuildComponentCallback` | `() => (request, options) => React.ComponentType \| false \| null \| undefined` | Callback hook which return a function to build the component for rendering the result |
164205
165206
### Options
166207
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/** @jest-environment jsdom */
2+
/// <reference types="@types/jest" />
3+
4+
import { render } from '@testing-library/react';
5+
import React, { Fragment } from 'react';
6+
7+
import createChainOfResponsibility from './createChainOfResponsibility';
8+
9+
type Props = { children?: never };
10+
11+
test('two providers of chain of responsibility nested should render', () => {
12+
// GIVEN: One chain of responsibility.
13+
const { Provider, Proxy, types } = createChainOfResponsibility<undefined, Props, number>();
14+
15+
const middleware1: readonly (typeof types.middleware)[] = Object.freeze([
16+
init => next => request => {
17+
const Next = next(request);
18+
19+
return props => (
20+
<Fragment>
21+
Third{init} {Next && <Next {...props} />}
22+
</Fragment>
23+
);
24+
}
25+
]);
26+
const middleware2: readonly (typeof types.middleware)[] = Object.freeze([
27+
init => next => request => {
28+
const Next = next(request);
29+
30+
return props => (
31+
<Fragment>
32+
First{init} {Next && <Next {...props} />}
33+
</Fragment>
34+
);
35+
},
36+
init => next => request => {
37+
const Next = next(request);
38+
39+
return props => (
40+
<Fragment>
41+
Second{init} {Next && <Next {...props} />}
42+
</Fragment>
43+
);
44+
}
45+
]);
46+
47+
const FallbackComponent = () => <Fragment>End</Fragment>;
48+
49+
// WHEN: Render <Proxy> with same providers twice.
50+
const App = ({
51+
middleware1,
52+
middleware2
53+
}: {
54+
middleware1: readonly (typeof types.middleware)[];
55+
middleware2?: readonly (typeof types.middleware)[] | undefined;
56+
}) => (
57+
<Provider init={1} middleware={middleware1}>
58+
{middleware2 ? (
59+
<Provider init={2} middleware={middleware2}>
60+
<Proxy fallbackComponent={FallbackComponent} />
61+
</Provider>
62+
) : (
63+
<Proxy fallbackComponent={FallbackComponent} />
64+
)}
65+
</Provider>
66+
);
67+
68+
const result = render(<App middleware1={middleware1} middleware2={middleware2} />);
69+
70+
// THEN: It should render "First2 Second2 Third1 ".
71+
expect(result.container).toHaveProperty('textContent', 'First2 Second2 Third1 End');
72+
73+
// WHEN: First middleware is updated.
74+
const middleware3: readonly (typeof types.middleware)[] = Object.freeze([
75+
init => next => request => {
76+
const Next = next(request);
77+
78+
return props => (
79+
<Fragment>
80+
Fourth{init} {Next && <Next {...props} />}
81+
</Fragment>
82+
);
83+
}
84+
]);
85+
86+
result.rerender(<App middleware1={middleware3} middleware2={middleware2} />);
87+
88+
// THEN: It should render "First2 Second2 Fourth1 ".
89+
expect(result.container).toHaveProperty('textContent', 'First2 Second2 Fourth1 End');
90+
91+
// WHEN: Second provider is removed middleware is updated.
92+
result.rerender(<App middleware1={middleware3} />);
93+
94+
// THEN: It should render "Fourth1 ".
95+
expect(result.container).toHaveProperty('textContent', 'Fourth1 End');
96+
});

packages/react-chain-of-responsibility/src/createChainOfResponsibility.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,21 @@ import React, {
1111
} from 'react';
1212

1313
import isReactComponent from './isReactComponent';
14-
import applyMiddleware from './private/applyMiddleware';
14+
import applyMiddleware, { type Enhancer } from './private/applyMiddleware';
1515
import { type ComponentMiddleware } from './types';
1616

17-
type UseBuildComponentCallbackOptions<Props> = { fallbackComponent?: ComponentType<Props> | false | null | undefined };
17+
type ResultComponent<Props> = ComponentType<Props> | false | null | undefined;
18+
19+
type UseBuildComponentCallbackOptions<Props> = { fallbackComponent?: ResultComponent<Props> };
1820

1921
type UseBuildComponentCallback<Request, Props> = (
2022
request: Request,
2123
options?: UseBuildComponentCallbackOptions<Props>
22-
) => ComponentType<Props> | false | null | undefined;
24+
) => ResultComponent<Props>;
2325

2426
type ProviderContext<Request, Props> = {
25-
useBuildComponentCallback: UseBuildComponentCallback<Request, Props>;
27+
get enhancer(): Enhancer<[Request], ResultComponent<Props>> | undefined;
28+
get useBuildComponentCallback(): UseBuildComponentCallback<Request, Props>;
2629
};
2730

2831
type ProviderProps<Request, Props, Init> = PropsWithChildren<{
@@ -63,11 +66,16 @@ export default function createChainOfResponsibility<
6366
};
6467
useBuildComponentCallback: () => UseBuildComponentCallback<Request, Props>;
6568
} {
66-
const context = createContext<ProviderContext<Request, Props>>({
69+
const defaultUseBuildComponentCallback: ProviderContext<Request, Props> = {
70+
get enhancer() {
71+
return undefined;
72+
},
6773
get useBuildComponentCallback(): ProviderContext<Request, Props>['useBuildComponentCallback'] {
6874
throw new Error('useBuildComponentCallback() hook cannot be used outside of its corresponding <Provider>');
6975
}
70-
});
76+
};
77+
78+
const context = createContext<ProviderContext<Request, Props>>(defaultUseBuildComponentCallback);
7179

7280
const Provider: ComponentType<ProviderProps<Request, Props, Init>> = ({ children, init, middleware }) => {
7381
// TODO: Related to https://github.com/microsoft/TypeScript/issues/17002.
@@ -122,15 +130,17 @@ export default function createChainOfResponsibility<
122130
: []
123131
);
124132

133+
const { enhancer: parentEnhancer } = useContext(context);
134+
125135
const enhancer = useMemo(
126136
() =>
127137
// We are reversing because it is easier to read:
128138
// - With reverse, [a, b, c] will become a(b(c(fn)))
129139
// - Without reverse, [a, b, c] will become c(b(a(fn)))
130-
applyMiddleware<[Request], ComponentType<Props> | false | null | undefined, [Init]>(
131-
...[...patchedMiddleware].reverse()
140+
applyMiddleware<[Request], ResultComponent<Props>, [Init]>(
141+
...[...patchedMiddleware, ...(parentEnhancer ? [() => parentEnhancer] : [])].reverse()
132142
)(init as Init),
133-
[init, middleware]
143+
[init, middleware, parentEnhancer]
134144
);
135145

136146
const useBuildComponentCallback = useCallback<UseBuildComponentCallback<Request, Props>>(
@@ -139,8 +149,8 @@ export default function createChainOfResponsibility<
139149
);
140150

141151
const contextValue = useMemo<ProviderContext<Request, Props>>(
142-
() => ({ useBuildComponentCallback }),
143-
[useBuildComponentCallback]
152+
() => ({ enhancer, useBuildComponentCallback }),
153+
[enhancer, useBuildComponentCallback]
144154
);
145155

146156
return <context.Provider value={contextValue}>{children}</context.Provider>;

0 commit comments

Comments
 (0)