Skip to content
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- (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)
- (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)
- Use `handler-chain` package, by [@compulim](https://github.com/compulim) in PR [#93](https://github.com/compulim/react-chain-of-responsibility/pull/93)
- Bumped dependencies, in PR [#97](https://github.com/compulim/react-chain-of-responsibility/pull/97)
- Development dependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import React, {
import { custom, function_, object, parse, safeParse } from 'valibot';

import arePropsEqual from './private/arePropsEqual.ts';
import useMemoValueWithEquality from './private/useMemoValueWithEquality.ts';

// TODO: Related to https://github.com/microsoft/TypeScript/issues/17002.
// typescript@5.2.2 has a bug, Array.isArray() is a type predicate but only works with mutable array, not readonly array.
Expand Down Expand Up @@ -105,7 +104,7 @@ type BuildContextType<Request, Props extends BaseProps> = {
};

type RenderContextType<Props> = {
readonly renderCallbackProps: Props;
readonly originalProps: Props;
};

type ProviderProps<Request, Props extends BaseProps, Init> = PropsWithChildren<{
Expand Down Expand Up @@ -200,7 +199,7 @@ function createChainOfResponsibility<
readonly overridingProps?: Partial<Props> | undefined;
}) {
const { allowOverrideProps } = options;
const { renderCallbackProps } = useContext(RenderContext);
const { originalProps: renderCallbackProps } = useContext(RenderContext);

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

const RENDER_CALLBACK_SYMBOL = `REACT_CHAIN_OF_RESPONSIBILITY:DO_NOT_USE_THIS_RENDER_CALLBACK`;

const useBuildRenderCallback: () => UseBuildRenderCallback<Request, Props> = () => {
const { enhancer } = useContext(BuildContext);

Expand Down Expand Up @@ -246,22 +247,37 @@ function createChainOfResponsibility<

return (
result &&
((props: Props) => {
const renderCallbackProps = useMemoValueWithEquality<Props>(() => props, arePropsEqual);

const context = useMemo<RenderContextType<Props>>(
() => Object.freeze({ renderCallbackProps }),
[renderCallbackProps]
);

return <RenderContext.Provider value={context}>{result.render()}</RenderContext.Provider>;
})
((props: Props) => (
// This is render function, we cannot call any hooks here.
<RenderCallbackAsComponent
{...props} // Spreading the props to leverage React.memo()
{...{
// TODO: Verify if `result.render` is stable or not, and check performance
[RENDER_CALLBACK_SYMBOL]: result.render
}}
/>
))
);
},
[enhancer]
);
};

type RenderCallbackAsComponentProps = Props & {
// First render function does not need overrideProps.
// Override props is for upstreamer to override props before passing to downsteamers.
readonly [RENDER_CALLBACK_SYMBOL]: () => ReactNode;
};

const RenderCallbackAsComponent = memo(function RenderFunction({
[RENDER_CALLBACK_SYMBOL]: render,
...props
}: RenderCallbackAsComponentProps) {
const context = useMemo<RenderContextType<Props>>(() => Object.freeze({ originalProps: props as Props }), [props]);

return <RenderContext.Provider value={context}>{render()}</RenderContext.Provider>;
});

function ChainOfResponsibilityProvider({ children, init, middleware }: ProviderProps<Request, Props, Init>) {
if (!Array.isArray(middleware) || middleware.some(middleware => typeof middleware !== 'function')) {
throw new Error('react-chain-of-responsibility: "middleware" prop must be an array of functions');
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/** @jest-environment jsdom */
/// <reference types="@types/jest" />

import { scenario } from '@testduet/given-when-then';
import { render } from '@testing-library/react';
import React, { Fragment, useState } from 'react';

import createChainOfResponsibility, { type InferMiddleware } from '../createChainOfResponsibilityAsRenderCallback';

type Props = { readonly children?: never; value: number };
type Request = string;

type MyComponentProps = Props;

function OddComponent({ value }: MyComponentProps) {
const [state] = useState(value);

return <Fragment>Odd ({state})</Fragment>;
}

function EvenComponent({ value }: MyComponentProps) {
const [state] = useState(value);

return <Fragment>Even ({state})</Fragment>;
}

scenario('useBuildRenderCallback', bdd => {
bdd
.given('a TestComponent using chain of responsiblity', () => {
const { Provider, reactComponent, useBuildRenderCallback } = createChainOfResponsibility<Request, Props>();

const middleware: readonly InferMiddleware<typeof Provider>[] = [
() => next => request => {
if (request) {
return reactComponent(request === 'Odd' ? OddComponent : EvenComponent, {});
}

return next(request);
}
];

function App({ values }: { readonly values: readonly number[] }) {
const render = useBuildRenderCallback();
const renderOdd = render('Odd');
const renderEven = render('Even');

return (
<Fragment>
{values.map(value => (
<Fragment key={value}>{value % 2 ? renderOdd?.({ value }) : renderEven?.({ value })}</Fragment>
))}
</Fragment>
);
}

return function TestComponent({ values }: { readonly values: readonly number[] }) {
return (
<Provider middleware={middleware}>
<App values={values} />
</Provider>
);
};
})
.when('the component is rendered', TestComponent => render(<TestComponent values={[1, 2, 3, 4]} />))
.then('textContent should match', (_, { container }) =>
expect(container).toHaveProperty('textContent', 'Odd (1)Even (2)Odd (3)Even (4)')
)
.when('the component is re-rendered with more components', (TestComponent, result) => {
result.rerender(<TestComponent values={[1, 2, 3, 4, 5, 6]} />);

return result;
})
.then('textContent should match', (_, { container }) =>
expect(container).toHaveProperty('textContent', 'Odd (1)Even (2)Odd (3)Even (4)Odd (5)Even (6)')
);
});