Skip to content

Commit

Permalink
Don't bail out on referential equality of Consumer's props.children f…
Browse files Browse the repository at this point in the history
…unction (#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
  • Loading branch information
iamdustan authored and gaearon committed Mar 29, 2018
1 parent 5855e9f commit 15e3dff
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 23 deletions.
6 changes: 3 additions & 3 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -994,10 +994,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
changedBits,
renderExpirationTime,
);
} else if (oldProps !== null && oldProps.children === newProps.children) {
// No change. Bailout early if children are the same.
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
// There is no bailout on `children` equality because we expect people
// to often pass a bound method as a child, but it may reference
// `this.state` or `this.props` (and thus needs to re-render on `setState`).

const render = newProps.children;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -727,39 +727,111 @@ describe('ReactNewContext', () => {
expect(ReactNoop.getChildren()).toEqual([span('Child')]);
});

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

function Child() {
ReactNoop.yield('Child');
return <span prop="Child" />;
}

function renderConsumer(context) {
return <Child context={context} />;
function renderChildValue(value) {
ReactNoop.yield('Consumer');
return <span prop={value} />;
}

function App(props) {
ReactNoop.yield('App');
function ChildWithInlineRenderCallback() {
ReactNoop.yield('ChildWithInlineRenderCallback');
// Note: we are intentionally passing an inline arrow. Don't refactor.
return (
<Context.Provider value={props.value}>
<Context.Consumer>{renderConsumer}</Context.Consumer>
</Context.Provider>
<Context.Consumer>{value => renderChildValue(value)}</Context.Consumer>
);
}

// Initial mount
ReactNoop.render(<App value={1} />);
expect(ReactNoop.flush()).toEqual(['App', 'Child']);
expect(ReactNoop.getChildren()).toEqual([span('Child')]);
function ChildWithCachedRenderCallback() {
ReactNoop.yield('ChildWithCachedRenderCallback');
return <Context.Consumer>{renderChildValue}</Context.Consumer>;
}

// Update
class PureIndirection extends React.PureComponent {
render() {
ReactNoop.yield('PureIndirection');
return (
<React.Fragment>
<ChildWithInlineRenderCallback />
<ChildWithCachedRenderCallback />
</React.Fragment>
);
}
}

class App extends React.Component {
render() {
ReactNoop.yield('App');
return (
<Context.Provider value={this.props.value}>
<PureIndirection />
</Context.Provider>
);
}
}

// Initial mount
ReactNoop.render(<App value={1} />);
expect(ReactNoop.flush()).toEqual([
'App',
// Child does not re-render
'PureIndirection',
'ChildWithInlineRenderCallback',
'Consumer',
'ChildWithCachedRenderCallback',
'Consumer',
]);
expect(ReactNoop.getChildren()).toEqual([span('Child')]);
expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);

// Update (bailout)
ReactNoop.render(<App value={1} />);
expect(ReactNoop.flush()).toEqual(['App']);
expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);

// Update (no bailout)
ReactNoop.render(<App value={2} />);
expect(ReactNoop.flush()).toEqual(['App', 'Consumer', 'Consumer']);
expect(ReactNoop.getChildren()).toEqual([span(2), span(2)]);
});

// Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
// However, it doesn't bail out from rendering if the component above it re-rendered anyway.
// If we bailed out on referential equality, it would be confusing that you
// can call this.setState(), but an autobound render callback "blocked" the update.
// https://github.com/facebook/react/pull/12470#issuecomment-376917711
it('consumer does not bail out if there were no bailouts above it', () => {
const Context = React.createContext(0);

class App extends React.Component {
state = {
text: 'hello',
};

renderConsumer = context => {
ReactNoop.yield('App#renderConsumer');
return <span prop={this.state.text} />;
};

render() {
ReactNoop.yield('App');
return (
<Context.Provider value={this.props.value}>
<Context.Consumer>{this.renderConsumer}</Context.Consumer>
</Context.Provider>
);
}
}

// Initial mount
let inst;
ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
expect(ReactNoop.getChildren()).toEqual([span('hello')]);

// Update
inst.setState({text: 'goodbye'});
expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
});

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

0 comments on commit 15e3dff

Please sign in to comment.