Skip to content

Commit 4f9e7bf

Browse files
authored
Fix preview/useBuildRenderCallback with hook usage (#99)
* Fix useBuildRenderCallback with hook usage * Add entry
1 parent d864102 commit 4f9e7bf

File tree

4 files changed

+106
-29
lines changed

4 files changed

+106
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
### Changed
1515

16-
- (Preview) 💢 Changed signature to return wrapped return value, instead of plain `ComponentType`, by [@compulim](https://github.com/compulim) in PR [#91](https://github.com/compulim/react-chain-of-responsibility/pull/91) and [#92](https://github.com/compulim/react-chain-of-responsibility/pull/92)
16+
- (Preview) 💢 Changed signature to return wrapped return value, instead of plain `ComponentType`, by [@compulim](https://github.com/compulim) in PR [#91](https://github.com/compulim/react-chain-of-responsibility/pull/91), [#92](https://github.com/compulim/react-chain-of-responsibility/pull/92), and [#99](https://github.com/compulim/react-chain-of-responsibility/pull/99)
1717
- Use `handler-chain` package, by [@compulim](https://github.com/compulim) in PR [#93](https://github.com/compulim/react-chain-of-responsibility/pull/93)
1818
- Bumped dependencies, in PR [#97](https://github.com/compulim/react-chain-of-responsibility/pull/97)
1919
- Development dependencies

packages/react-chain-of-responsibility/src/preview/createChainOfResponsibilityAsRenderCallback.tsx

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import React, {
1313
import { custom, function_, object, parse, safeParse } from 'valibot';
1414

1515
import arePropsEqual from './private/arePropsEqual.ts';
16-
import useMemoValueWithEquality from './private/useMemoValueWithEquality.ts';
1716

1817
// TODO: Related to https://github.com/microsoft/TypeScript/issues/17002.
1918
// [email protected] has a bug, Array.isArray() is a type predicate but only works with mutable array, not readonly array.
@@ -105,7 +104,7 @@ type BuildContextType<Request, Props extends BaseProps> = {
105104
};
106105

107106
type RenderContextType<Props> = {
108-
readonly renderCallbackProps: Props;
107+
readonly originalProps: Props;
109108
};
110109

111110
type ProviderProps<Request, Props extends BaseProps, Init> = PropsWithChildren<{
@@ -200,7 +199,7 @@ function createChainOfResponsibility<
200199
readonly overridingProps?: Partial<Props> | undefined;
201200
}) {
202201
const { allowOverrideProps } = options;
203-
const { renderCallbackProps } = useContext(RenderContext);
202+
const { originalProps: renderCallbackProps } = useContext(RenderContext);
204203

205204
if (overridingProps && !arePropsEqual(overridingProps, renderCallbackProps) && !allowOverrideProps) {
206205
console.warn('react-chain-of-responsibility: "allowOverrideProps" must be set to true to override props');
@@ -213,6 +212,8 @@ function createChainOfResponsibility<
213212
return <Component {...props} {...(typeof bindProps === 'function' ? bindProps(props) : bindProps)} />;
214213
});
215214

215+
const RENDER_CALLBACK_SYMBOL = `REACT_CHAIN_OF_RESPONSIBILITY:DO_NOT_USE_THIS_RENDER_CALLBACK`;
216+
216217
const useBuildRenderCallback: () => UseBuildRenderCallback<Request, Props> = () => {
217218
const { enhancer } = useContext(BuildContext);
218219

@@ -246,22 +247,37 @@ function createChainOfResponsibility<
246247

247248
return (
248249
result &&
249-
((props: Props) => {
250-
const renderCallbackProps = useMemoValueWithEquality<Props>(() => props, arePropsEqual);
251-
252-
const context = useMemo<RenderContextType<Props>>(
253-
() => Object.freeze({ renderCallbackProps }),
254-
[renderCallbackProps]
255-
);
256-
257-
return <RenderContext.Provider value={context}>{result.render()}</RenderContext.Provider>;
258-
})
250+
((props: Props) => (
251+
// This is render function, we cannot call any hooks here.
252+
<RenderCallbackAsComponent
253+
{...props} // Spreading the props to leverage React.memo()
254+
{...{
255+
// TODO: Verify if `result.render` is stable or not, and check performance
256+
[RENDER_CALLBACK_SYMBOL]: result.render
257+
}}
258+
/>
259+
))
259260
);
260261
},
261262
[enhancer]
262263
);
263264
};
264265

266+
type RenderCallbackAsComponentProps = Props & {
267+
// First render function does not need overrideProps.
268+
// Override props is for upstreamer to override props before passing to downsteamers.
269+
readonly [RENDER_CALLBACK_SYMBOL]: () => ReactNode;
270+
};
271+
272+
const RenderCallbackAsComponent = memo(function RenderFunction({
273+
[RENDER_CALLBACK_SYMBOL]: render,
274+
...props
275+
}: RenderCallbackAsComponentProps) {
276+
const context = useMemo<RenderContextType<Props>>(() => Object.freeze({ originalProps: props as Props }), [props]);
277+
278+
return <RenderContext.Provider value={context}>{render()}</RenderContext.Provider>;
279+
});
280+
265281
function ChainOfResponsibilityProvider({ children, init, middleware }: ProviderProps<Request, Props, Init>) {
266282
if (!Array.isArray(middleware) || middleware.some(middleware => typeof middleware !== 'function')) {
267283
throw new Error('react-chain-of-responsibility: "middleware" prop must be an array of functions');

packages/react-chain-of-responsibility/src/preview/private/useMemoValueWithEquality.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/** @jest-environment jsdom */
2+
/// <reference types="@types/jest" />
3+
4+
import { scenario } from '@testduet/given-when-then';
5+
import { render } from '@testing-library/react';
6+
import React, { Fragment, useState } from 'react';
7+
8+
import createChainOfResponsibility, { type InferMiddleware } from '../createChainOfResponsibilityAsRenderCallback';
9+
10+
type Props = { readonly children?: never; value: number };
11+
type Request = string;
12+
13+
type MyComponentProps = Props;
14+
15+
function OddComponent({ value }: MyComponentProps) {
16+
const [state] = useState(value);
17+
18+
return <Fragment>Odd ({state})</Fragment>;
19+
}
20+
21+
function EvenComponent({ value }: MyComponentProps) {
22+
const [state] = useState(value);
23+
24+
return <Fragment>Even ({state})</Fragment>;
25+
}
26+
27+
scenario('useBuildRenderCallback', bdd => {
28+
bdd
29+
.given('a TestComponent using chain of responsiblity', () => {
30+
const { Provider, reactComponent, useBuildRenderCallback } = createChainOfResponsibility<Request, Props>();
31+
32+
const middleware: readonly InferMiddleware<typeof Provider>[] = [
33+
() => next => request => {
34+
if (request) {
35+
return reactComponent(request === 'Odd' ? OddComponent : EvenComponent, {});
36+
}
37+
38+
return next(request);
39+
}
40+
];
41+
42+
function App({ values }: { readonly values: readonly number[] }) {
43+
const render = useBuildRenderCallback();
44+
const renderOdd = render('Odd');
45+
const renderEven = render('Even');
46+
47+
return (
48+
<Fragment>
49+
{values.map(value => (
50+
<Fragment key={value}>{value % 2 ? renderOdd?.({ value }) : renderEven?.({ value })}</Fragment>
51+
))}
52+
</Fragment>
53+
);
54+
}
55+
56+
return function TestComponent({ values }: { readonly values: readonly number[] }) {
57+
return (
58+
<Provider middleware={middleware}>
59+
<App values={values} />
60+
</Provider>
61+
);
62+
};
63+
})
64+
.when('the component is rendered', TestComponent => render(<TestComponent values={[1, 2, 3, 4]} />))
65+
.then('textContent should match', (_, { container }) =>
66+
expect(container).toHaveProperty('textContent', 'Odd (1)Even (2)Odd (3)Even (4)')
67+
)
68+
.when('the component is re-rendered with more components', (TestComponent, result) => {
69+
result.rerender(<TestComponent values={[1, 2, 3, 4, 5, 6]} />);
70+
71+
return result;
72+
})
73+
.then('textContent should match', (_, { container }) =>
74+
expect(container).toHaveProperty('textContent', 'Odd (1)Even (2)Odd (3)Even (4)Odd (5)Even (6)')
75+
);
76+
});

0 commit comments

Comments
 (0)