diff --git a/.changeset/yellow-pans-raise.md b/.changeset/yellow-pans-raise.md
new file mode 100644
index 0000000..0f54237
--- /dev/null
+++ b/.changeset/yellow-pans-raise.md
@@ -0,0 +1,20 @@
+---
+"react-fast-hoc": patch
+---
+
+Fixed wrapping into hoc in case of nested `React.memo`
+Resolves: [#17](https://github.com/XantreGodlike/react-fast-hoc/issues/17)
+
+```tsx
+const Component = transformProps(
+ React.memo(
+ React.memo((props) => {
+ console.log(props); // prev: { c: 30 }, now: { a: 10, b: 20, c: 30 }
+ return null;
+ })
+ ),
+ (props) => ({ ...props, a: 10, b: 20 })
+);
+
+root.render();
+```
diff --git a/packages/react-fast-hoc/src/index.test.ts b/packages/react-fast-hoc/src/__tests__/index.test.ts
similarity index 86%
rename from packages/react-fast-hoc/src/index.test.ts
rename to packages/react-fast-hoc/src/__tests__/index.test.ts
index 0bbdd45..b5ea18c 100644
--- a/packages/react-fast-hoc/src/index.test.ts
+++ b/packages/react-fast-hoc/src/__tests__/index.test.ts
@@ -4,7 +4,8 @@ import { Objects } from "hotscript";
import React, { ComponentType, createElement, forwardRef, memo } from "react";
import { Function } from "ts-toolbelt";
import { afterEach, describe, expect, expectTypeOf, test, vi } from "vitest";
-import { createTransformProps, transformProps, wrapIntoProxy } from ".";
+import { createTransformProps, transformProps, wrapIntoProxy } from "..";
+import { applyHocs, lazyShort, renderComponent } from "./utils";
declare module "react" {
export interface ExoticComponent {
@@ -102,17 +103,29 @@ describe("transformProps", () => {
});
});
- test("works with memo after hoc", () => {
- render(createElement(memo(addBebeHoc(Component))));
- standardCheck();
- });
- test("works with memo before hoc", () => {
- render(createElement(addBebeHoc(memo(Component))));
+ describe("hocs: memo", () => {
+ test("memo -> hoc", () => {
+ renderComponent(applyHocs(Component, [React.memo, addBebeHoc]));
+ standardCheck();
+ });
+ test("hoc -> memo", () => {
+ renderComponent(applyHocs(Component, [addBebeHoc, React.memo]));
- expect(Component).toHaveBeenCalledOnce();
- expect(addBebeProp).toHaveBeenCalledOnce();
- expect(addBebeProp).toHaveBeenCalledWith({});
- expect(Component).toHaveBeenCalledWith({ bebe: true }, {});
+ expect(Component).toHaveBeenCalledOnce();
+ expect(addBebeProp).toHaveBeenCalledOnce();
+ expect(addBebeProp).toHaveBeenCalledWith({});
+ expect(Component).toHaveBeenCalledWith({ bebe: true }, {});
+ });
+ test("hoc -> memo -> memo", () => {
+ renderComponent(
+ applyHocs(Component, [addBebeHoc, React.memo, React.memo])
+ );
+
+ expect(Component).toHaveBeenCalledOnce();
+ expect(addBebeProp).toHaveBeenCalledOnce();
+ expect(addBebeProp).toHaveBeenCalledWith({});
+ expect(Component).toHaveBeenCalledWith({ bebe: true }, {});
+ });
});
// this
@@ -138,7 +151,7 @@ describe("transformProps", () => {
describe("hocs: lazy", () => {
test("unloaded lazy", async () => {
const Cmp = vi.fn(Component);
- const Lazy = React.lazy(() => Promise.resolve({ default: Cmp }));
+ const Lazy = lazyShort(Cmp);
render(
createElement(React.Suspense, {}, createElement(addBebeHoc(Lazy)))
diff --git a/packages/react-fast-hoc/src/mimicToNewComponent.test.ts b/packages/react-fast-hoc/src/__tests__/mimicToNewComponent.test.ts
similarity index 95%
rename from packages/react-fast-hoc/src/mimicToNewComponent.test.ts
rename to packages/react-fast-hoc/src/__tests__/mimicToNewComponent.test.ts
index 10a2027..95a9a73 100644
--- a/packages/react-fast-hoc/src/mimicToNewComponent.test.ts
+++ b/packages/react-fast-hoc/src/__tests__/mimicToNewComponent.test.ts
@@ -1,5 +1,5 @@
import { assert, beforeEach, describe, expect, test } from "vitest";
-import { transformProps } from ".";
+import { transformProps } from "..";
const createComponent = () => () => null;
diff --git a/packages/react-fast-hoc/src/react.test.ts b/packages/react-fast-hoc/src/__tests__/react.test.ts
similarity index 87%
rename from packages/react-fast-hoc/src/react.test.ts
rename to packages/react-fast-hoc/src/__tests__/react.test.ts
index 9d2ff07..331394f 100644
--- a/packages/react-fast-hoc/src/react.test.ts
+++ b/packages/react-fast-hoc/src/__tests__/react.test.ts
@@ -1,25 +1,23 @@
-import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
+import {
+ afterAll,
+ afterEach,
+ beforeAll,
+ describe,
+ expect,
+ it,
+ vi,
+} from "vitest";
import { act, cleanup, render, waitFor } from "@testing-library/react";
import { range, sleep, tryit } from "radash";
import React, { ComponentType, createElement } from "react";
-
-const renderComponent = (Component: React.ComponentType) =>
- render(createElement(Component));
-
-const lazyShort = >(component: T) =>
- React.lazy(() =>
- Promise.resolve({
- default: component,
- })
- );
-
-const applyHocs = >(
- Component: T,
- hocs: ((...args: any) => any)[]
-): T => hocs.reduceRight((acc, cur) => cur(acc), Component);
+import { applyHocs, lazyShort, renderComponent } from "./utils";
function noop() {}
+afterEach(() => {
+ cleanup();
+});
+
/**
* oportunities
* memo = 1..9999 memos
diff --git a/packages/react-fast-hoc/src/rename.test.ts b/packages/react-fast-hoc/src/__tests__/rename.test.ts
similarity index 98%
rename from packages/react-fast-hoc/src/rename.test.ts
rename to packages/react-fast-hoc/src/__tests__/rename.test.ts
index a4967d5..b7fdd3e 100644
--- a/packages/react-fast-hoc/src/rename.test.ts
+++ b/packages/react-fast-hoc/src/__tests__/rename.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
-import { transformProps } from ".";
+import { transformProps } from "..";
describe("renaming works", () => {
const A = () => null;
diff --git a/packages/react-fast-hoc/src/handlers/rewriteCall.test.ts b/packages/react-fast-hoc/src/__tests__/rewriteCall.test.ts
similarity index 92%
rename from packages/react-fast-hoc/src/handlers/rewriteCall.test.ts
rename to packages/react-fast-hoc/src/__tests__/rewriteCall.test.ts
index 79b5396..7f3da59 100644
--- a/packages/react-fast-hoc/src/handlers/rewriteCall.test.ts
+++ b/packages/react-fast-hoc/src/__tests__/rewriteCall.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, test, vi } from "vitest";
-import { RewriteCall } from "./rewriteCall";
+import { RewriteCall } from "../handlers/rewriteCall";
describe("rewrite call should rewrite it", () => {
const f = vi.fn();
diff --git a/packages/react-fast-hoc/src/__tests__/utils.ts b/packages/react-fast-hoc/src/__tests__/utils.ts
new file mode 100644
index 0000000..39bf1da
--- /dev/null
+++ b/packages/react-fast-hoc/src/__tests__/utils.ts
@@ -0,0 +1,20 @@
+import {
+ RenderResult,
+ render,
+} from "@testing-library/react";
+import React, { ComponentType, createElement } from "react";
+
+export const renderComponent = (Component: React.ComponentType): RenderResult =>
+ render(createElement(Component));
+
+export const lazyShort = >(component: T) =>
+ React.lazy(() =>
+ Promise.resolve({
+ default: component,
+ })
+ );
+
+export const applyHocs = >(
+ Component: T,
+ hocs: ((...args: any) => any)[]
+): T => hocs.reduceRight((acc, cur) => cur(acc), Component);
diff --git a/packages/react-fast-hoc/src/internals.ts b/packages/react-fast-hoc/src/internals.ts
index e1a90c8..518132b 100644
--- a/packages/react-fast-hoc/src/internals.ts
+++ b/packages/react-fast-hoc/src/internals.ts
@@ -1,4 +1,4 @@
-import { isValidElement, type ReactNode, type Ref } from "react";
+import React, { isValidElement, type ReactNode, type Ref } from "react";
import type { HocTransformer, MimicToNewComponentHandler } from "./handlers";
import { isClassComponent, toFunctional, type Get } from "./toFunctional";
@@ -43,7 +43,7 @@ type RealComponentType =
| {
$$typeof: typeof REACT_MEMO_TYPE;
compare: null | ((a: TProps, b: TProps) => boolean);
- type: (props: TProps) => ReactNode;
+ type: RealComponentType;
}
| {
$$typeof: typeof REACT_LAZY_TYPE;
@@ -65,28 +65,34 @@ type ReactFunctionalComponentType<
{ $$typeof: typeof REACT_FORWARD_REF_TYPE } | React.FC
>;
-const wrapFunctionalFROrDefault = (
+type ForwardRefComponent = Extract<
+ ReactFunctionalComponentType,
+ { $$typeof: typeof REACT_FORWARD_REF_TYPE }
+>;
+type RegularFunctionComponent = Exclude<
+ ReactFunctionalComponentType,
+ ForwardRefComponent
+>;
+const wrapFCWithForwardRefOrPlain = (
Component: ReactFunctionalComponentType,
handler: HocTransformer
-) => {
- type ForwardRefComponent = Extract<
- ReactFunctionalComponentType,
- { $$typeof: typeof REACT_FORWARD_REF_TYPE }
- >;
- type RegularFunctionComponent = Exclude<
- ReactFunctionalComponentType,
- ForwardRefComponent
- >;
+): ForwardRefComponent | ReactFunctionalComponentType => {
if (
"$$typeof" in Component &&
Component["$$typeof"] === REACT_FORWARD_REF_TYPE
) {
return {
$$typeof: REACT_FORWARD_REF_TYPE,
- render: new Proxy((Component as ForwardRefComponent).render, handler),
- };
+ render: new Proxy(
+ (Component as ForwardRefComponent).render,
+ handler
+ ),
+ } as ForwardRefComponent;
}
- return new Proxy(Component as RegularFunctionComponent, handler);
+ return new Proxy(
+ Component as Function,
+ handler
+ ) as RegularFunctionComponent;
};
// I don't know why but typescript is not helpful at all
@@ -105,13 +111,14 @@ export const wrapComponentIntoHoc = (
handler: HocTransformer,
mimicToNewComponentHandler: null | MimicToNewComponentHandler
): unknown => {
- if (process.env.NODE_ENV === "development" && !isValidElement(Component)) {
- console.warn("react-fast-hoc: passed incorrect component for transform");
- return Component;
- }
+ // should use isValidElementType
+ // if (process.env.NODE_ENV === "development" && !isValidElement(Component)) {
+ // console.warn("react-fast-hoc: passed incorrect component for transform");
+ // return Component;
+ // }
// this case assumes that it's ClassComponent
if (isClassComponent(Component)) {
- return wrapFunctionalFROrDefault(
+ return wrapFCWithForwardRefOrPlain(
toFunctional(Component) as React.FC,
handler
);
@@ -120,8 +127,7 @@ export const wrapComponentIntoHoc = (
if ("$$typeof" in Component && Component["$$typeof"] === REACT_MEMO_TYPE) {
return {
$$typeof: REACT_MEMO_TYPE,
- // @ts-expect-error
- type: wrapFunctionalFROrDefault(toFunctional(Component.type), handler),
+ type: wrapComponentIntoHoc(Component.type, handler, null),
compare: Component.compare,
};
}
@@ -147,7 +153,7 @@ export const wrapComponentIntoHoc = (
result = wrapComponentIntoHoc(
initRes,
handler,
- mimicToNewComponentHandler
+ null
) as RealComponentType;
}
return result;
diff --git a/packages/react-fast-hoc/src/reactShallowEqual.ts b/packages/react-fast-hoc/src/reactShallowEqual.ts
new file mode 100644
index 0000000..19bb1d2
--- /dev/null
+++ b/packages/react-fast-hoc/src/reactShallowEqual.ts
@@ -0,0 +1,44 @@
+// https://github.com/facebook/react/blob/b9244f79bab133e0e6d2e992b5be4722b4ac7536/packages/shared/shallowEqual.js
+const is = Object.is;
+const hasOwnProperty = Object.prototype.hasOwnProperty;
+
+/**
+ * Performs equality by iterating through keys on an object and returning false
+ * when any key has values which are not strictly equal between the arguments.
+ * Returns true when the values of all keys are strictly equal.
+ */
+export function reactShallowEqual(objA: unknown, objB: unknown): boolean {
+ if (is(objA, objB)) {
+ return true;
+ }
+
+ if (
+ typeof objA !== "object" ||
+ objA === null ||
+ typeof objB !== "object" ||
+ objB === null
+ ) {
+ return false;
+ }
+
+ const keysA = Object.keys(objA);
+ const keysB = Object.keys(objB);
+
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+
+ // Test for A's keys different from B.
+ for (let i = 0; i < keysA.length; i++) {
+ const currentKey = keysA[i];
+ if (
+ !hasOwnProperty.call(objB, currentKey) ||
+ // @ts-expect-error lost refinement of `objB`
+ !is(objA[currentKey], objB[currentKey])
+ ) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/packages/react-fast-hoc/src/toFunctional.ts b/packages/react-fast-hoc/src/toFunctional.ts
index 1783d1a..1208072 100644
--- a/packages/react-fast-hoc/src/toFunctional.ts
+++ b/packages/react-fast-hoc/src/toFunctional.ts
@@ -46,7 +46,9 @@ const isPrototypeOf = Function.call.bind(Object.prototype.isPrototypeOf) as (
child: unknown
) => boolean;
-export const isClassComponent = (
+export const isClassComponent = isPrototypeOf.bind(
+ isPrototypeOf,
+ ReactComponent
+) as (
Component: T
-): Component is Extract> =>
- isPrototypeOf(ReactComponent, Component);
+) => Component is Extract>;