Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a68ca9a

Browse files
authored
React.pure automatically forwards ref (facebook#13822)
We're not planning to encourage legacy context, and without this change, it's difficult to use pure+forwardRef together. We could special-case `pure(forwardRef(...))` but this is hopefully simpler. ```js React.pure(function(props, ref) { // ... }); ```
1 parent 0af8199 commit a68ca9a

File tree

2 files changed

+161
-101
lines changed

2 files changed

+161
-101
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ function updatePureComponent(
236236
renderExpirationTime: ExpirationTime,
237237
) {
238238
const render = Component.render;
239+
const ref = workInProgress.ref;
239240

240241
if (
241242
current !== null &&
@@ -246,7 +247,7 @@ function updatePureComponent(
246247
// Default to shallow comparison
247248
let compare = Component.compare;
248249
compare = compare !== null ? compare : shallowEqual;
249-
if (compare(prevProps, nextProps)) {
250+
if (workInProgress.ref === current.ref && compare(prevProps, nextProps)) {
250251
return bailoutOnAlreadyFinishedWork(
251252
current,
252253
workInProgress,
@@ -261,10 +262,10 @@ function updatePureComponent(
261262
if (__DEV__) {
262263
ReactCurrentOwner.current = workInProgress;
263264
ReactCurrentFiber.setCurrentPhase('render');
264-
nextChildren = render(nextProps);
265+
nextChildren = render(nextProps, ref);
265266
ReactCurrentFiber.setCurrentPhase(null);
266267
} else {
267-
nextChildren = render(nextProps);
268+
nextChildren = render(nextProps, ref);
268269
}
269270

270271
// React DevTools reads this flag.

packages/react-reconciler/src/__tests__/ReactPure-test.internal.js

Lines changed: 157 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@ describe('pure', () => {
3535
}
3636

3737
// Tests should run against both the lazy and non-lazy versions of `pure`.
38-
// To make the tests work for both versions, we wrap the non-lazy verion in
38+
// To make the tests work for both versions, we wrap the non-lazy version in
3939
// a lazy function component.
4040
sharedTests('normal', (...args) => {
4141
const Pure = React.pure(...args);
42-
function Indirection(props) {
43-
return <Pure {...props} />;
42+
function Indirection(props, ref) {
43+
return <Pure {...props} ref={ref} />;
4444
}
45-
return Promise.resolve(Indirection);
45+
return Promise.resolve(React.forwardRef(Indirection));
4646
});
4747
sharedTests('lazy', (...args) => Promise.resolve(React.pure(...args)));
4848

@@ -84,110 +84,169 @@ describe('pure', () => {
8484
expect(ReactNoop.flush()).toEqual([1]);
8585
expect(ReactNoop.getChildren()).toEqual([span(1)]);
8686
});
87-
});
8887

89-
it("does not bail out if there's a context change", async () => {
90-
const {unstable_Suspense: Suspense} = React;
91-
92-
const CountContext = React.createContext(0);
93-
94-
function Counter(props) {
95-
const count = CountContext.unstable_read();
96-
return <Text text={`${props.label}: ${count}`} />;
97-
}
98-
Counter = pure(Counter);
99-
100-
class Parent extends React.Component {
101-
state = {count: 0};
102-
render() {
103-
return (
104-
<Suspense>
105-
<CountContext.Provider value={this.state.count}>
106-
<Counter label="Count" />
107-
</CountContext.Provider>
108-
</Suspense>
88+
it("does not bail out if there's a context change", async () => {
89+
const {unstable_Suspense: Suspense} = React;
90+
91+
const CountContext = React.createContext(0);
92+
93+
function Counter(props) {
94+
const count = CountContext.unstable_read();
95+
return <Text text={`${props.label}: ${count}`} />;
96+
}
97+
Counter = pure(Counter);
98+
99+
class Parent extends React.Component {
100+
state = {count: 0};
101+
render() {
102+
return (
103+
<Suspense>
104+
<CountContext.Provider value={this.state.count}>
105+
<Counter label="Count" />
106+
</CountContext.Provider>
107+
</Suspense>
108+
);
109+
}
110+
}
111+
112+
const parent = React.createRef(null);
113+
ReactNoop.render(<Parent ref={parent} />);
114+
expect(ReactNoop.flush()).toEqual([]);
115+
await Promise.resolve();
116+
expect(ReactNoop.flush()).toEqual(['Count: 0']);
117+
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
118+
119+
// Should bail out because props have not changed
120+
ReactNoop.render(<Parent ref={parent} />);
121+
expect(ReactNoop.flush()).toEqual([]);
122+
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
123+
124+
// Should update because there was a context change
125+
parent.current.setState({count: 1});
126+
expect(ReactNoop.flush()).toEqual(['Count: 1']);
127+
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
128+
});
129+
130+
it('accepts custom comparison function', async () => {
131+
const {unstable_Suspense: Suspense} = React;
132+
133+
function Counter({count}) {
134+
return <Text text={count} />;
135+
}
136+
Counter = pure(Counter, (oldProps, newProps) => {
137+
ReactNoop.yield(
138+
`Old count: ${oldProps.count}, New count: ${newProps.count}`,
109139
);
140+
return oldProps.count === newProps.count;
141+
});
142+
143+
ReactNoop.render(
144+
<Suspense>
145+
<Counter count={0} />
146+
</Suspense>,
147+
);
148+
expect(ReactNoop.flush()).toEqual([]);
149+
await Promise.resolve();
150+
expect(ReactNoop.flush()).toEqual([0]);
151+
expect(ReactNoop.getChildren()).toEqual([span(0)]);
152+
153+
// Should bail out because props have not changed
154+
ReactNoop.render(
155+
<Suspense>
156+
<Counter count={0} />
157+
</Suspense>,
158+
);
159+
expect(ReactNoop.flush()).toEqual(['Old count: 0, New count: 0']);
160+
expect(ReactNoop.getChildren()).toEqual([span(0)]);
161+
162+
// Should update because count prop changed
163+
ReactNoop.render(
164+
<Suspense>
165+
<Counter count={1} />
166+
</Suspense>,
167+
);
168+
expect(ReactNoop.flush()).toEqual(['Old count: 0, New count: 1', 1]);
169+
expect(ReactNoop.getChildren()).toEqual([span(1)]);
170+
});
171+
172+
it('warns for class components', () => {
173+
class SomeClass extends React.Component {
174+
render() {
175+
return null;
176+
}
110177
}
111-
}
112-
113-
const parent = React.createRef(null);
114-
ReactNoop.render(<Parent ref={parent} />);
115-
expect(ReactNoop.flush()).toEqual([]);
116-
await Promise.resolve();
117-
expect(ReactNoop.flush()).toEqual(['Count: 0']);
118-
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
119-
120-
// Should bail out because props have not changed
121-
ReactNoop.render(<Parent ref={parent} />);
122-
expect(ReactNoop.flush()).toEqual([]);
123-
expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
124-
125-
// Should update because there was a context change
126-
parent.current.setState({count: 1});
127-
expect(ReactNoop.flush()).toEqual(['Count: 1']);
128-
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
129-
});
178+
expect(() => pure(SomeClass)).toWarnDev(
179+
'pure: The first argument must be a function component.',
180+
{withoutStack: true},
181+
);
182+
});
130183

131-
it('accepts custom comparison function', async () => {
132-
const {unstable_Suspense: Suspense} = React;
184+
it('warns if first argument is not a function', () => {
185+
expect(() => pure()).toWarnDev(
186+
'pure: The first argument must be a function component. Instead ' +
187+
'received: undefined',
188+
{withoutStack: true},
189+
);
190+
});
133191

134-
function Counter({count}) {
135-
return <Text text={count} />;
136-
}
137-
Counter = pure(Counter, (oldProps, newProps) => {
138-
ReactNoop.yield(
139-
`Old count: ${oldProps.count}, New count: ${newProps.count}`,
192+
it('forwards ref', async () => {
193+
const {unstable_Suspense: Suspense} = React;
194+
const Transparent = pure((props, ref) => {
195+
return <div ref={ref} />;
196+
});
197+
const divRef = React.createRef();
198+
199+
ReactNoop.render(
200+
<Suspense>
201+
<Transparent ref={divRef} />
202+
</Suspense>,
140203
);
141-
return oldProps.count === newProps.count;
204+
ReactNoop.flush();
205+
await Promise.resolve();
206+
ReactNoop.flush();
207+
expect(divRef.current.type).toBe('div');
142208
});
143209

144-
ReactNoop.render(
145-
<Suspense>
146-
<Counter count={0} />
147-
</Suspense>,
148-
);
149-
expect(ReactNoop.flush()).toEqual([]);
150-
await Promise.resolve();
151-
expect(ReactNoop.flush()).toEqual([0]);
152-
expect(ReactNoop.getChildren()).toEqual([span(0)]);
153-
154-
// Should bail out because props have not changed
155-
ReactNoop.render(
156-
<Suspense>
157-
<Counter count={0} />
158-
</Suspense>,
159-
);
160-
expect(ReactNoop.flush()).toEqual(['Old count: 0, New count: 0']);
161-
expect(ReactNoop.getChildren()).toEqual([span(0)]);
162-
163-
// Should update because count prop changed
164-
ReactNoop.render(
165-
<Suspense>
166-
<Counter count={1} />
167-
</Suspense>,
168-
);
169-
expect(ReactNoop.flush()).toEqual(['Old count: 0, New count: 1', 1]);
170-
expect(ReactNoop.getChildren()).toEqual([span(1)]);
171-
});
210+
it('updates if only ref changes', async () => {
211+
const {unstable_Suspense: Suspense} = React;
212+
const Transparent = pure((props, ref) => {
213+
return [<Text key="text" text="Text" />, <div key="div" ref={ref} />];
214+
});
172215

173-
it('warns for class components', () => {
174-
class SomeClass extends React.Component {
175-
render() {
176-
return null;
177-
}
178-
}
179-
expect(() => pure(SomeClass)).toWarnDev(
180-
'pure: The first argument must be a function component.',
181-
{withoutStack: true},
182-
);
183-
});
216+
const divRef = React.createRef();
217+
const divRef2 = React.createRef();
218+
219+
ReactNoop.render(
220+
<Suspense>
221+
<Transparent ref={divRef} />
222+
</Suspense>,
223+
);
224+
expect(ReactNoop.flush()).toEqual([]);
225+
await Promise.resolve();
226+
expect(ReactNoop.flush()).toEqual(['Text']);
227+
expect(divRef.current.type).toBe('div');
228+
expect(divRef2.current).toBe(null);
229+
230+
// Should re-render (new ref)
231+
ReactNoop.render(
232+
<Suspense>
233+
<Transparent ref={divRef2} />
234+
</Suspense>,
235+
);
236+
expect(ReactNoop.flush()).toEqual(['Text']);
237+
expect(divRef.current).toBe(null);
238+
expect(divRef2.current.type).toBe('div');
184239

185-
it('warns if first argument is not a function', () => {
186-
expect(() => pure()).toWarnDev(
187-
'pure: The first argument must be a function component. Instead ' +
188-
'received: undefined',
189-
{withoutStack: true},
190-
);
240+
// Should not re-render (same ref)
241+
ReactNoop.render(
242+
<Suspense>
243+
<Transparent ref={divRef2} />
244+
</Suspense>,
245+
);
246+
expect(ReactNoop.flush()).toEqual([]);
247+
expect(divRef.current).toBe(null);
248+
expect(divRef2.current.type).toBe('div');
249+
});
191250
});
192251
}
193252
});

0 commit comments

Comments
 (0)