Skip to content

Commit 15e3dff

Browse files
iamdustangaearon
authored andcommitted
Don't bail out on referential equality of Consumer's props.children function (#12470)
* Test case for React Context bailing out unexpectedly * This is 💯% definitely not the correct fix at all * Revert "This is 💯% definitely not the correct fix at all" This reverts commit 8686c0f. * Formatting + minor tweaks to the test * Don't bail out on consumer child equality * Tweak the comment * Pretty lint * Silly Dan
1 parent 5855e9f commit 15e3dff

File tree

2 files changed

+95
-23
lines changed

2 files changed

+95
-23
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -994,10 +994,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
994994
changedBits,
995995
renderExpirationTime,
996996
);
997-
} else if (oldProps !== null && oldProps.children === newProps.children) {
998-
// No change. Bailout early if children are the same.
999-
return bailoutOnAlreadyFinishedWork(current, workInProgress);
1000997
}
998+
// There is no bailout on `children` equality because we expect people
999+
// to often pass a bound method as a child, but it may reference
1000+
// `this.state` or `this.props` (and thus needs to re-render on `setState`).
10011001

10021002
const render = newProps.children;
10031003

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

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -727,39 +727,111 @@ describe('ReactNewContext', () => {
727727
expect(ReactNoop.getChildren()).toEqual([span('Child')]);
728728
});
729729

730-
it('consumer bails out if children and value are unchanged (like sCU)', () => {
730+
it('consumer bails out if value is unchanged and something above bailed out', () => {
731731
const Context = React.createContext(0);
732732

733-
function Child() {
734-
ReactNoop.yield('Child');
735-
return <span prop="Child" />;
736-
}
737-
738-
function renderConsumer(context) {
739-
return <Child context={context} />;
733+
function renderChildValue(value) {
734+
ReactNoop.yield('Consumer');
735+
return <span prop={value} />;
740736
}
741737

742-
function App(props) {
743-
ReactNoop.yield('App');
738+
function ChildWithInlineRenderCallback() {
739+
ReactNoop.yield('ChildWithInlineRenderCallback');
740+
// Note: we are intentionally passing an inline arrow. Don't refactor.
744741
return (
745-
<Context.Provider value={props.value}>
746-
<Context.Consumer>{renderConsumer}</Context.Consumer>
747-
</Context.Provider>
742+
<Context.Consumer>{value => renderChildValue(value)}</Context.Consumer>
748743
);
749744
}
750745

751-
// Initial mount
752-
ReactNoop.render(<App value={1} />);
753-
expect(ReactNoop.flush()).toEqual(['App', 'Child']);
754-
expect(ReactNoop.getChildren()).toEqual([span('Child')]);
746+
function ChildWithCachedRenderCallback() {
747+
ReactNoop.yield('ChildWithCachedRenderCallback');
748+
return <Context.Consumer>{renderChildValue}</Context.Consumer>;
749+
}
755750

756-
// Update
751+
class PureIndirection extends React.PureComponent {
752+
render() {
753+
ReactNoop.yield('PureIndirection');
754+
return (
755+
<React.Fragment>
756+
<ChildWithInlineRenderCallback />
757+
<ChildWithCachedRenderCallback />
758+
</React.Fragment>
759+
);
760+
}
761+
}
762+
763+
class App extends React.Component {
764+
render() {
765+
ReactNoop.yield('App');
766+
return (
767+
<Context.Provider value={this.props.value}>
768+
<PureIndirection />
769+
</Context.Provider>
770+
);
771+
}
772+
}
773+
774+
// Initial mount
757775
ReactNoop.render(<App value={1} />);
758776
expect(ReactNoop.flush()).toEqual([
759777
'App',
760-
// Child does not re-render
778+
'PureIndirection',
779+
'ChildWithInlineRenderCallback',
780+
'Consumer',
781+
'ChildWithCachedRenderCallback',
782+
'Consumer',
761783
]);
762-
expect(ReactNoop.getChildren()).toEqual([span('Child')]);
784+
expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
785+
786+
// Update (bailout)
787+
ReactNoop.render(<App value={1} />);
788+
expect(ReactNoop.flush()).toEqual(['App']);
789+
expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
790+
791+
// Update (no bailout)
792+
ReactNoop.render(<App value={2} />);
793+
expect(ReactNoop.flush()).toEqual(['App', 'Consumer', 'Consumer']);
794+
expect(ReactNoop.getChildren()).toEqual([span(2), span(2)]);
795+
});
796+
797+
// Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
798+
// However, it doesn't bail out from rendering if the component above it re-rendered anyway.
799+
// If we bailed out on referential equality, it would be confusing that you
800+
// can call this.setState(), but an autobound render callback "blocked" the update.
801+
// https://github.com/facebook/react/pull/12470#issuecomment-376917711
802+
it('consumer does not bail out if there were no bailouts above it', () => {
803+
const Context = React.createContext(0);
804+
805+
class App extends React.Component {
806+
state = {
807+
text: 'hello',
808+
};
809+
810+
renderConsumer = context => {
811+
ReactNoop.yield('App#renderConsumer');
812+
return <span prop={this.state.text} />;
813+
};
814+
815+
render() {
816+
ReactNoop.yield('App');
817+
return (
818+
<Context.Provider value={this.props.value}>
819+
<Context.Consumer>{this.renderConsumer}</Context.Consumer>
820+
</Context.Provider>
821+
);
822+
}
823+
}
824+
825+
// Initial mount
826+
let inst;
827+
ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
828+
expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
829+
expect(ReactNoop.getChildren()).toEqual([span('hello')]);
830+
831+
// Update
832+
inst.setState({text: 'goodbye'});
833+
expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
834+
expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
763835
});
764836

765837
// This is a regression case for https://github.com/facebook/react/issues/12389.

0 commit comments

Comments
 (0)