Skip to content

Commit

Permalink
Merge pull request #18 from XantreGodlike/fix-nested-memos
Browse files Browse the repository at this point in the history
Fixed nested memos
  • Loading branch information
XantreDev authored Oct 21, 2023
2 parents 617f5f5 + 32e18c1 commit da8f9b4
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 57 deletions.
20 changes: 20 additions & 0 deletions .changeset/yellow-pans-raise.md
Original file line number Diff line number Diff line change
@@ -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(<Component c={30} />);
```
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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)))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert, beforeEach, describe, expect, test } from "vitest";
import { transformProps } from ".";
import { transformProps } from "..";

const createComponent = () => () => null;

Expand Down
Original file line number Diff line number Diff line change
@@ -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 = <T extends ComponentType<any>>(component: T) =>
React.lazy(() =>
Promise.resolve({
default: component,
})
);

const applyHocs = <T extends ComponentType<any>>(
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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { transformProps } from ".";
import { transformProps } from "..";

describe("renaming works", () => {
const A = () => null;
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
20 changes: 20 additions & 0 deletions packages/react-fast-hoc/src/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends ComponentType<any>>(component: T) =>
React.lazy(() =>
Promise.resolve({
default: component,
})
);

export const applyHocs = <T extends ComponentType<any>>(
Component: T,
hocs: ((...args: any) => any)[]
): T => hocs.reduceRight((acc, cur) => cur(acc), Component);
52 changes: 29 additions & 23 deletions packages/react-fast-hoc/src/internals.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -43,7 +43,7 @@ type RealComponentType<TProps extends object, IRef = unknown> =
| {
$$typeof: typeof REACT_MEMO_TYPE;
compare: null | ((a: TProps, b: TProps) => boolean);
type: (props: TProps) => ReactNode;
type: RealComponentType<TProps, IRef>;
}
| {
$$typeof: typeof REACT_LAZY_TYPE;
Expand All @@ -65,28 +65,34 @@ type ReactFunctionalComponentType<
{ $$typeof: typeof REACT_FORWARD_REF_TYPE } | React.FC<TProps>
>;

const wrapFunctionalFROrDefault = <TProps extends object>(
type ForwardRefComponent<TProps extends object> = Extract<
ReactFunctionalComponentType<TProps>,
{ $$typeof: typeof REACT_FORWARD_REF_TYPE }
>;
type RegularFunctionComponent<TProps extends object> = Exclude<
ReactFunctionalComponentType<TProps>,
ForwardRefComponent<TProps>
>;
const wrapFCWithForwardRefOrPlain = <TProps extends object>(
Component: ReactFunctionalComponentType<TProps>,
handler: HocTransformer
) => {
type ForwardRefComponent = Extract<
ReactFunctionalComponentType<TProps>,
{ $$typeof: typeof REACT_FORWARD_REF_TYPE }
>;
type RegularFunctionComponent = Exclude<
ReactFunctionalComponentType<TProps>,
ForwardRefComponent
>;
): ForwardRefComponent<TProps> | ReactFunctionalComponentType<TProps> => {
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<TProps>).render,
handler
),
} as ForwardRefComponent<TProps>;
}
return new Proxy(Component as RegularFunctionComponent, handler);
return new Proxy(
Component as Function,
handler
) as RegularFunctionComponent<TProps>;
};

// I don't know why but typescript is not helpful at all
Expand All @@ -105,13 +111,14 @@ export const wrapComponentIntoHoc = <TProps extends object>(
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<TProps>,
handler
);
Expand All @@ -120,8 +127,7 @@ export const wrapComponentIntoHoc = <TProps extends object>(
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,
};
}
Expand All @@ -147,7 +153,7 @@ export const wrapComponentIntoHoc = <TProps extends object>(
result = wrapComponentIntoHoc(
initRes,
handler,
mimicToNewComponentHandler
null
) as RealComponentType<any>;
}
return result;
Expand Down
44 changes: 44 additions & 0 deletions packages/react-fast-hoc/src/reactShallowEqual.ts
Original file line number Diff line number Diff line change
@@ -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;
}
8 changes: 5 additions & 3 deletions packages/react-fast-hoc/src/toFunctional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ const isPrototypeOf = Function.call.bind(Object.prototype.isPrototypeOf) as (
child: unknown
) => boolean;

export const isClassComponent = <T>(
export const isClassComponent = isPrototypeOf.bind(
isPrototypeOf,
ReactComponent
) as <T>(
Component: T
): Component is Extract<T, React.ComponentClass<any, any>> =>
isPrototypeOf(ReactComponent, Component);
) => Component is Extract<T, React.ComponentClass<any, any>>;

0 comments on commit da8f9b4

Please sign in to comment.