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
30 changes: 24 additions & 6 deletions src/Immutable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import * as React from 'react';

const ImmutableContext = React.createContext<number>(0);

export type CompareProps<T extends React.ComponentType<any>> = (
prevProps: Readonly<React.ComponentProps<T>>,
nextProps: Readonly<React.ComponentProps<T>>,
) => boolean;

/**
* Get render update mark by `makeImmutable` root.
* Do not deps on the return value as render times
Expand All @@ -16,14 +21,30 @@ export function useImmutableMark() {
* Wrapped Component will be marked as Immutable.
* When Component parent trigger render,
* it will notice children component (use with `responseImmutable`) node that parent has updated.

* @param Component Passed Component
* @param triggerRender Customize trigger `responseImmutable` children re-render logic. Default will always trigger re-render when this component re-render.
*/
export function makeImmutable<T extends React.ComponentType<any>>(Component: T): T {
export function makeImmutable<T extends React.ComponentType<any>>(
Component: T,
shouldTriggerRender?: CompareProps<T>,
): T {
const refAble = supportRef(Component);

const ImmutableComponent = function (props: any, ref: any) {
const refProps = refAble ? { ref } : {};
const renderTimesRef = React.useRef(0);
renderTimesRef.current += 1;
const prevProps = React.useRef(props);

if (
// Always trigger re-render if not provide `notTriggerRender`
!shouldTriggerRender ||
shouldTriggerRender(prevProps.current, props)
) {
renderTimesRef.current += 1;
}

prevProps.current = props;

return (
<ImmutableContext.Provider value={renderTimesRef.current}>
Expand All @@ -45,10 +66,7 @@ export function makeImmutable<T extends React.ComponentType<any>>(Component: T):
*/
export function responseImmutable<T extends React.ComponentType<any>>(
Component: T,
propsAreEqual?: (
prevProps: Readonly<React.ComponentProps<T>>,
nextProps: Readonly<React.ComponentProps<T>>,
) => boolean,
propsAreEqual?: CompareProps<T>,
): T {
const refAble = supportRef(Component);

Expand Down
2 changes: 1 addition & 1 deletion tests/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function Value({ id, value }: { id?: string; value: any }) {

return (
<div id={id} className="value">
{str.replace(/^"/, '').replace(/"$/, '')}
{(str || '').replace(/^"/, '').replace(/"$/, '')}
</div>
);
}
47 changes: 47 additions & 0 deletions tests/immutable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,53 @@ describe('Immutable', () => {
expect(container.querySelector('#raw')!.textContent).toEqual('3');
expect(container.querySelector('#value')!.textContent).toEqual('1');
});

it('customize re-render logic', () => {
const MyRoot = ({
children,
}: {
children?: React.ReactNode;
trigger?: string;
notTrigger?: string;
}) => {
return (
<>
<RenderTimer id="root" />
{children}
</>
);
};

const ImmutableMyRoot = makeImmutable(MyRoot, (prev, next) => {
return prev.trigger !== next.trigger;
});

const { container, rerender } = render(
<ImmutableMyRoot>
<ImmutableRaw />
</ImmutableMyRoot>,
);
expect(container.querySelector('#root').textContent).toEqual('1');
expect(container.querySelector('#raw').textContent).toEqual('1');

// Update `notTrigger`: No Update
rerender(
<ImmutableMyRoot notTrigger="bamboo">
<ImmutableRaw />
</ImmutableMyRoot>,
);
expect(container.querySelector('#root').textContent).toEqual('2');
expect(container.querySelector('#raw').textContent).toEqual('1');

// Update `trigger`: Full Update
rerender(
<ImmutableMyRoot trigger="little" notTrigger="bamboo">
<ImmutableRaw />
</ImmutableMyRoot>,
);
expect(container.querySelector('#root').textContent).toEqual('3');
expect(container.querySelector('#raw').textContent).toEqual('2');
});
});

it('ref-able', () => {
Expand Down