Skip to content

Commit fa3ca17

Browse files
authored
Merge pull request #24 from react-component/context-create
feat: support createImmutable
2 parents 003e37a + 61773dc commit fa3ca17

File tree

3 files changed

+146
-66
lines changed

3 files changed

+146
-66
lines changed

src/Immutable.tsx

Lines changed: 80 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,106 @@
11
import { supportRef } from 'rc-util/lib/ref';
22
import * as React from 'react';
33

4-
const ImmutableContext = React.createContext<number>(0);
5-
64
export type CompareProps<T extends React.ComponentType<any>> = (
75
prevProps: Readonly<React.ComponentProps<T>>,
86
nextProps: Readonly<React.ComponentProps<T>>,
97
) => boolean;
108

119
/**
12-
* Get render update mark by `makeImmutable` root.
13-
* Do not deps on the return value as render times
14-
* but only use for `useMemo` or `useCallback` deps.
10+
* Create Immutable pair for `makeImmutable` and `responseImmutable`.
1511
*/
16-
export function useImmutableMark() {
17-
return React.useContext(ImmutableContext);
18-
}
12+
export default function createImmutable() {
13+
const ImmutableContext = React.createContext<number>(null);
14+
15+
/**
16+
* Get render update mark by `makeImmutable` root.
17+
* Do not deps on the return value as render times
18+
* but only use for `useMemo` or `useCallback` deps.
19+
*/
20+
function useImmutableMark() {
21+
return React.useContext(ImmutableContext);
22+
}
1923

20-
/**
24+
/**
2125
* Wrapped Component will be marked as Immutable.
2226
* When Component parent trigger render,
2327
* it will notice children component (use with `responseImmutable`) node that parent has updated.
2428
2529
* @param Component Passed Component
2630
* @param triggerRender Customize trigger `responseImmutable` children re-render logic. Default will always trigger re-render when this component re-render.
2731
*/
28-
export function makeImmutable<T extends React.ComponentType<any>>(
29-
Component: T,
30-
shouldTriggerRender?: CompareProps<T>,
31-
): T {
32-
const refAble = supportRef(Component);
33-
34-
const ImmutableComponent = function (props: any, ref: any) {
35-
const refProps = refAble ? { ref } : {};
36-
const renderTimesRef = React.useRef(0);
37-
const prevProps = React.useRef(props);
38-
39-
if (
40-
// Always trigger re-render if not provide `notTriggerRender`
41-
!shouldTriggerRender ||
42-
shouldTriggerRender(prevProps.current, props)
43-
) {
44-
renderTimesRef.current += 1;
32+
function makeImmutable<T extends React.ComponentType<any>>(
33+
Component: T,
34+
shouldTriggerRender?: CompareProps<T>,
35+
): T {
36+
const refAble = supportRef(Component);
37+
38+
const ImmutableComponent = function (props: any, ref: any) {
39+
const refProps = refAble ? { ref } : {};
40+
const renderTimesRef = React.useRef(0);
41+
const prevProps = React.useRef(props);
42+
43+
// If parent has the context, we do not wrap it
44+
const mark = useImmutableMark();
45+
if (mark !== null) {
46+
return <Component {...props} {...refProps} />;
47+
}
48+
49+
if (
50+
// Always trigger re-render if not provide `notTriggerRender`
51+
!shouldTriggerRender ||
52+
shouldTriggerRender(prevProps.current, props)
53+
) {
54+
renderTimesRef.current += 1;
55+
}
56+
57+
prevProps.current = props;
58+
59+
return (
60+
<ImmutableContext.Provider value={renderTimesRef.current}>
61+
<Component {...props} {...refProps} />
62+
</ImmutableContext.Provider>
63+
);
64+
};
65+
66+
if (process.env.NODE_ENV !== 'production') {
67+
ImmutableComponent.displayName = `ImmutableRoot(${Component.displayName || Component.name})`;
4568
}
4669

47-
prevProps.current = props;
48-
49-
return (
50-
<ImmutableContext.Provider value={renderTimesRef.current}>
51-
<Component {...props} {...refProps} />
52-
</ImmutableContext.Provider>
53-
);
54-
};
55-
56-
if (process.env.NODE_ENV !== 'production') {
57-
ImmutableComponent.displayName = `ImmutableRoot(${Component.displayName || Component.name})`;
70+
return refAble ? React.forwardRef(ImmutableComponent) : (ImmutableComponent as any);
5871
}
5972

60-
return refAble ? React.forwardRef(ImmutableComponent) : (ImmutableComponent as any);
61-
}
62-
63-
/**
64-
* Wrapped Component with `React.memo`.
65-
* But will rerender when parent with `makeImmutable` rerender.
66-
*/
67-
export function responseImmutable<T extends React.ComponentType<any>>(
68-
Component: T,
69-
propsAreEqual?: CompareProps<T>,
70-
): T {
71-
const refAble = supportRef(Component);
72-
73-
const ImmutableComponent = function (props: any, ref: any) {
74-
const refProps = refAble ? { ref } : {};
75-
useImmutableMark();
76-
77-
return <Component {...props} {...refProps} />;
78-
};
73+
/**
74+
* Wrapped Component with `React.memo`.
75+
* But will rerender when parent with `makeImmutable` rerender.
76+
*/
77+
function responseImmutable<T extends React.ComponentType<any>>(
78+
Component: T,
79+
propsAreEqual?: CompareProps<T>,
80+
): T {
81+
const refAble = supportRef(Component);
82+
83+
const ImmutableComponent = function (props: any, ref: any) {
84+
const refProps = refAble ? { ref } : {};
85+
useImmutableMark();
86+
87+
return <Component {...props} {...refProps} />;
88+
};
89+
90+
if (process.env.NODE_ENV !== 'production') {
91+
ImmutableComponent.displayName = `ImmutableResponse(${
92+
Component.displayName || Component.name
93+
})`;
94+
}
7995

80-
if (process.env.NODE_ENV !== 'production') {
81-
ImmutableComponent.displayName = `ImmutableResponse(${
82-
Component.displayName || Component.name
83-
})`;
96+
return refAble
97+
? React.memo(React.forwardRef(ImmutableComponent), propsAreEqual)
98+
: (React.memo(ImmutableComponent, propsAreEqual) as any);
8499
}
85100

86-
return refAble
87-
? React.memo(React.forwardRef(ImmutableComponent), propsAreEqual)
88-
: (React.memo(ImmutableComponent, propsAreEqual) as any);
101+
return {
102+
makeImmutable,
103+
responseImmutable,
104+
useImmutableMark,
105+
};
89106
}

src/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import type { SelectorContext } from './context';
22
import { createContext, useContext } from './context';
3-
import { makeImmutable, responseImmutable, useImmutableMark } from './Immutable';
3+
import createImmutable from './Immutable';
44

5-
export { createContext, useContext, makeImmutable, responseImmutable, useImmutableMark };
5+
// For legacy usage, we export it directly
6+
const { makeImmutable, responseImmutable, useImmutableMark } = createImmutable();
7+
8+
export {
9+
createContext,
10+
useContext,
11+
createImmutable,
12+
makeImmutable,
13+
responseImmutable,
14+
useImmutableMark,
15+
};
616
export type { SelectorContext };

tests/immutable.test.tsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { fireEvent, render } from '@testing-library/react';
22
import React from 'react';
3-
import { createContext, makeImmutable, responseImmutable, useContext } from '../src';
3+
import {
4+
createContext,
5+
createImmutable,
6+
makeImmutable,
7+
responseImmutable,
8+
useContext,
9+
} from '../src';
410
import { RenderTimer, Value } from './common';
511

612
describe('Immutable', () => {
@@ -165,4 +171,51 @@ describe('Immutable', () => {
165171
rerender(<ImmutableInput value="not-same" onChange={() => {}} />);
166172
expect(container.querySelector('#input').textContent).toEqual('2');
167173
});
174+
175+
describe('createImmutable', () => {
176+
const { responseImmutable: responseCreatedImmutable, makeImmutable: makeCreatedImmutable } =
177+
createImmutable();
178+
179+
it('nest should follow root', () => {
180+
// child
181+
const Little = responseCreatedImmutable(() => <RenderTimer id="little" />);
182+
183+
// parent
184+
const Bamboo = makeCreatedImmutable(() => (
185+
<>
186+
<RenderTimer id="bamboo" />
187+
<Little />
188+
</>
189+
));
190+
191+
// root
192+
const Light = makeCreatedImmutable(() => {
193+
const [times, setTimes] = React.useState(0);
194+
195+
return (
196+
<>
197+
<button onClick={() => setTimes(i => i + 1)}>{times}</button>
198+
<RenderTimer id="light" />
199+
<Bamboo />
200+
</>
201+
);
202+
});
203+
204+
const { container, rerender } = render(<Light />);
205+
206+
for (let i = 0; i < 10; i += 1) {
207+
rerender(<Light />);
208+
}
209+
expect(container.querySelector('#light')!.textContent).toEqual('11');
210+
expect(container.querySelector('#bamboo')!.textContent).toEqual('11');
211+
expect(container.querySelector('#little')!.textContent).toEqual('11');
212+
213+
for (let i = 0; i < 10; i += 1) {
214+
fireEvent.click(container.querySelector('button')!);
215+
}
216+
expect(container.querySelector('#light')!.textContent).toEqual('21');
217+
expect(container.querySelector('#bamboo')!.textContent).toEqual('21');
218+
expect(container.querySelector('#little')!.textContent).toEqual('11');
219+
});
220+
});
168221
});

0 commit comments

Comments
 (0)