Skip to content

Commit

Permalink
Support <Provider>-less usage (#65)
Browse files Browse the repository at this point in the history
* Support provider-less usage

* Update PR number of provider-less usage

* Fix typing

* Use null for no rendering
  • Loading branch information
compulim committed Jun 15, 2024
1 parent 6bcfe2f commit 3cb47ab
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 21 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use `import { createChainOfResponsibility } from 'react-chain-of-responsibility'` instead
- `import { createChainOfResponsibilityForFluentUI } from 'react-chain-of-responsibility/fluentUI'` for Fluent UI renderer function
- Moved build tools from Babel to tsup/esbuild
- Outside of `<Provider>`, when `useBuildComponentCallback` and `<Proxy>` is used with `fallbackComponent`, they will render the fallback component and no longer throwing exception

### Added

- 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)
- Components will be built using middleware from `<Provider>` closer to the `<Proxy>` and fallback to those farther away
- Support `<Provider>`-less usage if `fallbackComponent` is specified, by [@compulim](https://github.com/compulim) in PR [#65](https://github.com/compulim/react-chain-of-responsibility/pull/65)

### Changed

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ type UseBuildComponentCallback<Request, Props> = (
) => ComponentType<Props> | false | null | undefined;
```
The `fallbackComponent` is a component which all unhandled requests will sink into.
The `fallbackComponent` is a component which all unhandled requests will sink into, including calls without ancestral `<Provider>`.
### API for Fluent UI
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/** @jest-environment jsdom */
/// <reference types="@types/jest" />

import { render } from '@testing-library/react';
import React from 'react';

import createChainOfResponsibility from './createChainOfResponsibility';

type Props = { children?: never };

let consoleErrorMock: jest.SpyInstance;

beforeEach(() => {
// Currently, there is no way to hide the caught exception thrown by render().
// We are mocking `console.log` to hide the exception.
consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => jest.fn());
});

afterEach(() => {
consoleErrorMock.mockRestore();
});

test('when calling useBuildComponentCallback() outside of its <Provider> with fallbackComponent should render', () => {
// GIVEN: useBuildComponentCallback() from a newly created chain of responsibility.
const { useBuildComponentCallback } = createChainOfResponsibility<undefined, Props>();

const Fallback = () => <div>Hello, World!</div>;

const App = () => {
const Component = useBuildComponentCallback()(undefined, { fallbackComponent: Fallback });

return Component ? <Component /> : null;
};

// WHEN: Render.
const result = render(<App />);

// THEN: Should render fallbackComponent.
expect(result.container).toHaveProperty('textContent', 'Hello, World!');
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/// <reference types="@types/jest" />

import { render } from '@testing-library/react';
import React, { Fragment } from 'react';
import React from 'react';

import createChainOfResponsibility from './createChainOfResponsibility';

Expand All @@ -25,14 +25,12 @@ test('when calling useBuildComponentCallback() outside of its <Provider> should
const { useBuildComponentCallback } = createChainOfResponsibility<undefined, Props>();

const App = () => {
useBuildComponentCallback();
const Component = useBuildComponentCallback()(undefined);

return <Fragment />;
return Component ? <Component /> : null;
};

// WHEN: Render.
// THEN: It should throw an error saying useBuildComponentCallback() hook cannot be used outside of its corresponding <Provider>.
expect(() => render(<App />)).toThrow(
'useBuildComponentCallback() hook cannot be used outside of its corresponding <Provider>'
);
// THEN: It should throw an error saying "This component/hook cannot be used outside of its corresponding <Provider>".
expect(() => render(<App />)).toThrow('This component/hook cannot be used outside of its corresponding <Provider>');
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ afterEach(() => {
consoleErrorMock.mockRestore();
});

test('when rendering <Proxy> outside of its <Provider> should throw', () => {
test('when rendering <Proxy> outside of its <Provider> with fallbackComponent should render', () => {
// GIVEN: A <Proxy> of a newly created chain of responsibility.
const { Proxy } = createChainOfResponsibility<undefined, Props>();

const Fallback = () => <div>Hello, World!</div>;

// WHEN: Render.
const result = render(<Proxy fallbackComponent={Fallback} />);

// THEN: It should throw an error saying <Proxy> cannot be used outside of its corresponding <Provider>.
expect(() => render(<Proxy />)).toThrow('<Proxy> cannot be used outside of its corresponding <Provider>');
expect(result.container).toHaveProperty('textContent', 'Hello, World!');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/** @jest-environment jsdom */
/// <reference types="@types/jest" />

import { render } from '@testing-library/react';
import React from 'react';

import createChainOfResponsibility from './createChainOfResponsibility';

type Props = { children?: never };

let consoleErrorMock: jest.SpyInstance;

beforeEach(() => {
// Currently, there is no way to hide the caught exception thrown by render().
// We are mocking `console.log` to hide the exception.
consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => jest.fn());
});

afterEach(() => {
consoleErrorMock.mockRestore();
});

test('when rendering <Proxy> outside of its <Provider> without fallbackComponent should throw', () => {
// GIVEN: A <Proxy> of a newly created chain of responsibility.
const { Proxy } = createChainOfResponsibility<undefined, Props>();

// WHEN: Render.
// THEN: It should throw an error saying "This component/hook cannot be used outside of its corresponding <Provider>".
expect(() => render(<Proxy />)).toThrow('This component/hook cannot be used outside of its corresponding <Provider>');
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type UseBuildComponentCallback<Request, Props> = (

type ProviderContext<Request, Props> = {
get enhancer(): Enhancer<[Request], ResultComponent<Props>> | undefined;
get useBuildComponentCallback(): UseBuildComponentCallback<Request, Props>;
useBuildComponentCallback: UseBuildComponentCallback<Request, Props>;
};

type ProviderProps<Request, Props, Init> = PropsWithChildren<{
Expand Down Expand Up @@ -70,8 +70,12 @@ export default function createChainOfResponsibility<
get enhancer() {
return undefined;
},
get useBuildComponentCallback(): ProviderContext<Request, Props>['useBuildComponentCallback'] {
throw new Error('useBuildComponentCallback() hook cannot be used outside of its corresponding <Provider>');
useBuildComponentCallback(_, options) {
if (options?.fallbackComponent) {
return options.fallbackComponent;
}

throw new Error('This component/hook cannot be used outside of its corresponding <Provider>');
}
};

Expand Down Expand Up @@ -169,14 +173,7 @@ export default function createChainOfResponsibility<
// False positive: "children" is not a prop.
// eslint-disable-next-line react/prop-types
({ children, fallbackComponent, request, ...props }) => {
let enhancer: ReturnType<typeof useBuildComponentCallback>;

try {
enhancer = useBuildComponentCallback();
} catch {
throw new Error('<Proxy> cannot be used outside of its corresponding <Provider>');
}

const enhancer = useBuildComponentCallback();
const Component = enhancer(request as Request, { fallbackComponent });

return Component ? <Component {...(props as Props)}>{children}</Component> : null;
Expand Down

0 comments on commit 3cb47ab

Please sign in to comment.