Skip to content

Commit

Permalink
Support memo() and other built-in components (#34)
Browse files Browse the repository at this point in the history
* Support memo and more component types

* Add entry

* Add test
  • Loading branch information
compulim authored Sep 25, 2023
1 parent 06fed67 commit cf1fc0d
Show file tree
Hide file tree
Showing 21 changed files with 268 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Updated `exports` field to workaround [TypeScript resolution bug](https://github.com/microsoft/TypeScript/issues/50762), by [@compulim](https://github.com/compulim), in PR [#20](https://github.com/compulim/react-chain-of-responsibility/pull/20)
- Fixed [#32](https://github.com/compulim/react-chain-of-responsibility/issues/32), readonly middleware array should not return type error, by [@compulim](https://github.com/compulim), in PR [#33](https://github.com/compulim/react-chain-of-responsibility/pull/33)
- Fixed [#29](https://github.com/compulim/react-chain-of-responsibility/issues/29), support `memo()` and other built-in components, by [@compulim](https://github.com/compulim), in PR [#34](https://github.com/compulim/react-chain-of-responsibility/pull/34)

### Changed

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

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

import createChainOfResponsibility from './createChainOfResponsibility';

type Props = { children?: ReactNode };

test('middleware using <Fragment> should render', () => {
// GIVEN: A middleware return a component that would render "Hello, World!".
const { Provider, Proxy } = createChainOfResponsibility<undefined, Props>();

// WHEN: Render <Proxy>.
const App = () => (
<Provider middleware={[() => () => () => Fragment]}>
<Proxy>Hello, World!</Proxy>
</Provider>
);

const result = render(<App />);

// THEN: It should render "Hello, World!".
expect(result.container).toHaveProperty('textContent', 'Hello, World!');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/** @jest-environment jsdom */
/// <reference types="@types/jest" />

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

import createChainOfResponsibility from './createChainOfResponsibility';

type Props = { children?: never; text: string };

const HelloWorldComponent = memo(({ text }: Props) => <Fragment>{text}</Fragment>);

test('middleware should render', () => {
// GIVEN: A middleware return a component that would render "Hello, World!".
const { Provider, Proxy } = createChainOfResponsibility<undefined, Props>();

// WHEN: Render <Proxy>.
const App = ({ text }: Props) => (
<Provider middleware={[() => () => () => HelloWorldComponent]}>
<Proxy text={text} />
</Provider>
);

const result = render(<App text="Hello, World!" />);

// THEN: It should render "Hello, World!".
expect(result.container).toHaveProperty('textContent', 'Hello, World!');

result.rerender(<App text="Aloha!" />);

// THEN: It should render "Aloha!".
expect(result.container).toHaveProperty('textContent', 'Aloha!');
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,28 @@ import React, { Fragment } from 'react';

import createChainOfResponsibility from './createChainOfResponsibility';

type Props = { children?: never };
type Props = { children?: never; text: string };

const HelloWorldComponent = ({ text }: Props) => <Fragment>{text}</Fragment>;

test('middleware should render', () => {
// GIVEN: A middleware return a component that would render "Hello, World!".
const { Provider, Proxy } = createChainOfResponsibility<undefined, Props>();

// WHEN: Render <Proxy>.
const App = () => (
<Provider middleware={[() => () => () => () => <Fragment>Hello, World!</Fragment>]}>
<Proxy />
const App = ({ text }: Props) => (
<Provider middleware={[() => () => () => HelloWorldComponent]}>
<Proxy text={text} />
</Provider>
);

const result = render(<App />);
const result = render(<App text="Hello, World!" />);

// THEN: It should render "Hello, World!".
expect(result.container).toHaveProperty('textContent', 'Hello, World!');

result.rerender(<App text="Aloha!" />);

// THEN: It should render "Aloha!".
expect(result.container).toHaveProperty('textContent', 'Aloha!');
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import PropTypes from 'prop-types';
import React, { ComponentType, createContext, isValidElement, memo, useCallback, useContext, useMemo } from 'react';

import React, {
type ComponentType,
createContext,
isValidElement,
memo,
type PropsWithChildren,
useCallback,
useContext,
useMemo
} from 'react';

import { type ComponentMiddleware } from './types';
import applyMiddleware from './private/applyMiddleware';

import type { ComponentMiddleware } from './types';
import type { PropsWithChildren } from 'react';
import isReactComponent from './isReactComponent';

type UseBuildComponentCallbackOptions<Props> = { fallbackComponent?: ComponentType<Props> | false | null | undefined };

Expand Down Expand Up @@ -100,12 +108,8 @@ export default function createChainOfResponsibility<
} else if (
returnValue !== false &&
returnValue !== null &&
typeof returnValue !== 'function' &&
typeof returnValue !== 'undefined' &&
// There are no definitive ways to check if an object is a React component or not.
// We are checking if the object has a render function (classic component).
// Note: "forwardRef()" returns plain object, not class instance.
!(typeof returnValue === 'object' && typeof returnValue['render'] === 'function')
!isReactComponent(returnValue)
) {
throw new Error(
'middleware must return false, null, undefined, function component, or class component'
Expand Down
70 changes: 70 additions & 0 deletions packages/react-chain-of-responsibility/src/isReactComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
type ComponentClass,
type ComponentType,
type Consumer,
type Fragment,
type FunctionComponent,
type Provider
} from 'react';

function isConsumer(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component: any
): component is Consumer<unknown> {
return component?.$$typeof?.toString() === 'Symbol(react.context)';
}

function isProvider(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component: any
): component is Provider<unknown> {
return component?.$$typeof?.toString() === 'Symbol(react.provider)';
}

function isFragment(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component: any
): component is typeof Fragment {
return component?.toString() === 'Symbol(react.fragment)';
}

function isFunctionComponent(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component: any
): component is FunctionComponent {
if (typeof component === 'function') {
return true;
}

return isPureFunctionComponent(component);
}

function isPureFunctionComponent(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component: any
): component is FunctionComponent {
return component?.$$typeof?.toString() === 'Symbol(react.memo)' && isFunctionComponent(component.type);
}

function isComponentClass(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component: any
): component is ComponentClass {
return typeof component === 'object' && typeof component?.['render'] === 'function';
}

// There are no definitive ways to check if an object is a React component or not.
// We are checking if the object has a render function (classic component).
// Note: "forwardRef()" returns plain object, not class instance.
export default function isReactComponent(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component: any
): component is ComponentType {
return (
isFunctionComponent(component) ||
isComponentClass(component) ||
isFragment(component) ||
isConsumer(component) ||
isProvider(component)
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import isReactComponent from '../isReactComponent';

test('array should return false', () => {
expect(isReactComponent([])).toBe(false);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import isReactComponent from '../isReactComponent';

test('boolean should return false', () => {
expect(isReactComponent(true)).toBe(false);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React, { Component } from 'react';

import isReactComponent from '../isReactComponent';

test('component class should return true', () => {
class ComponentClass extends Component {
render() {
return <div>Hello, World!</div>;
}
}

expect(isReactComponent(ComponentClass)).toBe(true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createContext } from 'react';

import isReactComponent from '../isReactComponent';

test('context provider should return true', () => {
const Context = createContext(undefined);

expect(isReactComponent(Context.Consumer)).toBe(true);
expect(isReactComponent(Context.Provider)).toBe(true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import isReactComponent from '../isReactComponent';

test('date should return false', () => {
expect(isReactComponent(new Date())).toBe(false);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import isReactComponent from '../isReactComponent';

test('false should return false', () => {
expect(isReactComponent(false)).toBe(false);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Fragment } from 'react';

import isReactComponent from '../isReactComponent';

test('fragment should return true', () => {
expect(isReactComponent(Fragment)).toBe(true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React, { forwardRef } from 'react';

import isReactComponent from '../isReactComponent';

test('function component with forwardRef should return true', () => {
const FunctionComponent = forwardRef(() => <div>Hello, World!</div>);

expect(isReactComponent(FunctionComponent)).toBe(true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React, { memo } from 'react';

import isReactComponent from '../isReactComponent';

test('function component should return true', () => {
const FunctionComponent = memo(() => <div>Hello, World!</div>);

FunctionComponent.displayName = 'FunctionComponent';

expect(isReactComponent(FunctionComponent)).toBe(true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';

import isReactComponent from '../isReactComponent';

test('function component should return true', () => {
const FunctionComponent = () => <div>Hello, World!</div>;

expect(isReactComponent(FunctionComponent)).toBe(true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import isReactComponent from '../isReactComponent';

test('null should return false', () => {
expect(isReactComponent(null)).toBe(false);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import isReactComponent from '../isReactComponent';

test('number should return false', () => {
expect(isReactComponent(0)).toBe(false);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import isReactComponent from '../isReactComponent';

test('object should return false', () => {
expect(isReactComponent({})).toBe(false);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React, { PureComponent } from 'react';

import isReactComponent from '../isReactComponent';

test('component class should return true', () => {
class ComponentClass extends PureComponent {
render() {
return <div>Hello, World!</div>;
}
}

expect(isReactComponent(ComponentClass)).toBe(true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import isReactComponent from '../isReactComponent';

test('undefined should return false', () => {
expect(isReactComponent(undefined)).toBe(false);
});

0 comments on commit cf1fc0d

Please sign in to comment.