Warn when second argument is passed to useCallback (#14729)

This commit is contained in:
Dan Abramov 2019-01-31 13:56:48 +00:00 committed by GitHub
parent 70d4075832
commit 51c07912ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 256 additions and 134 deletions

View File

@ -613,8 +613,114 @@ describe('ReactDOMServerHooks', () => {
});
describe('useContext', () => {
itThrowsWhenRendering(
'if used inside a class component',
async render => {
const Context = React.createContext({}, () => {});
class Counter extends React.Component {
render() {
let [count] = useContext(Context);
return <Text text={count} />;
}
}
return render(<Counter />);
},
'Hooks can only be called inside the body of a function component.',
);
});
itRenders(
'can use the same context multiple times in the same function',
async render => {
const Context = React.createContext({foo: 0, bar: 0, baz: 0});
function Provider(props) {
return (
<Context.Provider
value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
{props.children}
</Context.Provider>
);
}
function FooAndBar() {
const {foo} = useContext(Context);
const {bar} = useContext(Context);
return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
}
function Baz() {
const {baz} = useContext(Context);
return <Text text={'Baz: ' + baz} />;
}
class Indirection extends React.Component {
render() {
return this.props.children;
}
}
function App(props) {
return (
<div>
<Provider foo={props.foo} bar={props.bar} baz={props.baz}>
<Indirection>
<Indirection>
<FooAndBar />
</Indirection>
<Indirection>
<Baz />
</Indirection>
</Indirection>
</Provider>
</div>
);
}
const domNode = await render(<App foo={1} bar={3} baz={5} />);
expect(clearYields()).toEqual(['Foo: 1, Bar: 3', 'Baz: 5']);
expect(domNode.childNodes.length).toBe(2);
expect(domNode.firstChild.tagName).toEqual('SPAN');
expect(domNode.firstChild.textContent).toEqual('Foo: 1, Bar: 3');
expect(domNode.lastChild.tagName).toEqual('SPAN');
expect(domNode.lastChild.textContent).toEqual('Baz: 5');
},
);
itRenders('warns when bitmask is passed to useContext', async render => {
let Context = React.createContext('Hi');
function Foo() {
return <span>{useContext(Context, 1)}</span>;
}
const domNode = await render(<Foo />, 1);
expect(domNode.textContent).toBe('Hi');
});
describe('useDebugValue', () => {
itRenders('is a noop', async render => {
function Counter(props) {
const debugValue = useDebugValue(123);
return <Text text={typeof debugValue} />;
}
const domNode = await render(<Counter />);
expect(domNode.textContent).toEqual('undefined');
});
});
describe('readContext', () => {
function readContext(Context, observedBits) {
const dispatcher =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher.current;
return dispatcher.readContext(Context, observedBits);
}
itRenders(
'can use the same context multiple times in the same function',
'can read the same context multiple times in the same function',
async render => {
const Context = React.createContext(
{foo: 0, bar: 0, baz: 0},
@ -643,13 +749,13 @@ describe('ReactDOMServerHooks', () => {
}
function FooAndBar() {
const {foo} = useContext(Context, 0b001);
const {bar} = useContext(Context, 0b010);
const {foo} = readContext(Context, 0b001);
const {bar} = readContext(Context, 0b010);
return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
}
function Baz() {
const {baz} = useContext(Context, 0b100);
const {baz} = readContext(Context, 0b100);
return <Text text={'Baz: ' + baz} />;
}
@ -689,43 +795,6 @@ describe('ReactDOMServerHooks', () => {
},
);
itThrowsWhenRendering(
'if used inside a class component',
async render => {
const Context = React.createContext({}, () => {});
class Counter extends React.Component {
render() {
let [count] = useContext(Context);
return <Text text={count} />;
}
}
return render(<Counter />);
},
'Hooks can only be called inside the body of a function component.',
);
});
describe('useDebugValue', () => {
itRenders('is a noop', async render => {
function Counter(props) {
const debugValue = useDebugValue(123);
return <Text text={typeof debugValue} />;
}
const domNode = await render(<Counter />);
expect(domNode.textContent).toEqual('undefined');
});
});
describe('readContext', () => {
function readContext(Context, observedBits) {
const dispatcher =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher.current;
return dispatcher.readContext(Context, observedBits);
}
itRenders('with a warning inside useMemo and useReducer', async render => {
const Context = React.createContext(42);

View File

@ -51,7 +51,15 @@ describe('ReactNewContext', () => {
Context =>
function Consumer(props) {
const observedBits = props.unstable_observedBits;
const contextValue = useContext(Context, observedBits);
let contextValue;
expect(() => {
contextValue = useContext(Context, observedBits);
}).toWarnDev(
observedBits !== undefined
? 'useContext() second argument is reserved for future use in React. ' +
`Passing it is not supported. You passed: ${observedBits}.`
: [],
);
const render = props.children;
return render(contextValue);
},
@ -59,7 +67,15 @@ describe('ReactNewContext', () => {
sharedContextTests('useContext inside forwardRef component', Context =>
React.forwardRef(function Consumer(props, ref) {
const observedBits = props.unstable_observedBits;
const contextValue = useContext(Context, observedBits);
let contextValue;
expect(() => {
contextValue = useContext(Context, observedBits);
}).toWarnDev(
observedBits !== undefined
? 'useContext() second argument is reserved for future use in React. ' +
`Passing it is not supported. You passed: ${observedBits}.`
: [],
);
const render = props.children;
return render(contextValue);
}),
@ -67,7 +83,15 @@ describe('ReactNewContext', () => {
sharedContextTests('useContext inside memoized function component', Context =>
React.memo(function Consumer(props) {
const observedBits = props.unstable_observedBits;
const contextValue = useContext(Context, observedBits);
let contextValue;
expect(() => {
contextValue = useContext(Context, observedBits);
}).toWarnDev(
observedBits !== undefined
? 'useContext() second argument is reserved for future use in React. ' +
`Passing it is not supported. You passed: ${observedBits}.`
: [],
);
const render = props.children;
return render(contextValue);
}),
@ -1300,6 +1324,97 @@ describe('ReactNewContext', () => {
});
describe('readContext', () => {
it('can read the same context multiple times in the same function', () => {
const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => {
let result = 0;
if (a.foo !== b.foo) {
result |= 0b001;
}
if (a.bar !== b.bar) {
result |= 0b010;
}
if (a.baz !== b.baz) {
result |= 0b100;
}
return result;
});
function Provider(props) {
return (
<Context.Provider
value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
{props.children}
</Context.Provider>
);
}
function FooAndBar() {
const {foo} = readContext(Context, 0b001);
const {bar} = readContext(Context, 0b010);
return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
}
function Baz() {
const {baz} = readContext(Context, 0b100);
return <Text text={'Baz: ' + baz} />;
}
class Indirection extends React.Component {
shouldComponentUpdate() {
return false;
}
render() {
return this.props.children;
}
}
function App(props) {
return (
<Provider foo={props.foo} bar={props.bar} baz={props.baz}>
<Indirection>
<Indirection>
<FooAndBar />
</Indirection>
<Indirection>
<Baz />
</Indirection>
</Indirection>
</Provider>
);
}
ReactNoop.render(<App foo={1} bar={1} baz={1} />);
expect(ReactNoop.flush()).toEqual(['Foo: 1, Bar: 1', 'Baz: 1']);
expect(ReactNoop.getChildren()).toEqual([
span('Foo: 1, Bar: 1'),
span('Baz: 1'),
]);
// Update only foo
ReactNoop.render(<App foo={2} bar={1} baz={1} />);
expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 1']);
expect(ReactNoop.getChildren()).toEqual([
span('Foo: 2, Bar: 1'),
span('Baz: 1'),
]);
// Update only bar
ReactNoop.render(<App foo={2} bar={2} baz={1} />);
expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 2']);
expect(ReactNoop.getChildren()).toEqual([
span('Foo: 2, Bar: 2'),
span('Baz: 1'),
]);
// Update only baz
ReactNoop.render(<App foo={2} bar={2} baz={2} />);
expect(ReactNoop.flush()).toEqual(['Baz: 2']);
expect(ReactNoop.getChildren()).toEqual([
span('Foo: 2, Bar: 2'),
span('Baz: 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
@ -1374,95 +1489,20 @@ describe('ReactNewContext', () => {
});
describe('useContext', () => {
it('can use the same context multiple times in the same function', () => {
const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => {
let result = 0;
if (a.foo !== b.foo) {
result |= 0b001;
}
if (a.bar !== b.bar) {
result |= 0b010;
}
if (a.baz !== b.baz) {
result |= 0b100;
}
return result;
});
function Provider(props) {
return (
<Context.Provider
value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
{props.children}
</Context.Provider>
);
it('warns on array.map(useContext)', () => {
const Context = React.createContext(0);
function Foo() {
return [Context].map(useContext);
}
function FooAndBar() {
const {foo} = useContext(Context, 0b001);
const {bar} = useContext(Context, 0b010);
return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
}
function Baz() {
const {baz} = useContext(Context, 0b100);
return <Text text={'Baz: ' + baz} />;
}
class Indirection extends React.Component {
shouldComponentUpdate() {
return false;
}
render() {
return this.props.children;
}
}
function App(props) {
return (
<Provider foo={props.foo} bar={props.bar} baz={props.baz}>
<Indirection>
<Indirection>
<FooAndBar />
</Indirection>
<Indirection>
<Baz />
</Indirection>
</Indirection>
</Provider>
);
}
ReactNoop.render(<App foo={1} bar={1} baz={1} />);
expect(ReactNoop.flush()).toEqual(['Foo: 1, Bar: 1', 'Baz: 1']);
expect(ReactNoop.getChildren()).toEqual([
span('Foo: 1, Bar: 1'),
span('Baz: 1'),
]);
// Update only foo
ReactNoop.render(<App foo={2} bar={1} baz={1} />);
expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 1']);
expect(ReactNoop.getChildren()).toEqual([
span('Foo: 2, Bar: 1'),
span('Baz: 1'),
]);
// Update only bar
ReactNoop.render(<App foo={2} bar={2} baz={1} />);
expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 2']);
expect(ReactNoop.getChildren()).toEqual([
span('Foo: 2, Bar: 2'),
span('Baz: 1'),
]);
// Update only baz
ReactNoop.render(<App foo={2} bar={2} baz={2} />);
expect(ReactNoop.flush()).toEqual(['Baz: 2']);
expect(ReactNoop.getChildren()).toEqual([
span('Foo: 2, Bar: 2'),
span('Baz: 2'),
]);
ReactNoop.render(<Foo />);
expect(ReactNoop.flush).toWarnDev(
'useContext() second argument is reserved for future ' +
'use in React. Passing it is not supported. ' +
'You passed: 0.\n\n' +
'Did you call array.map(useContext)? ' +
'Calling Hooks inside a loop is not supported. ' +
'Learn more at https://fb.me/rules-of-hooks',
);
});
it('throws when used in a class component', () => {

View File

@ -24,10 +24,23 @@ function resolveDispatcher() {
export function useContext<T>(
Context: ReactContext<T>,
observedBits: number | boolean | void,
unstable_observedBits: number | boolean | void,
) {
const dispatcher = resolveDispatcher();
if (__DEV__) {
warning(
unstable_observedBits === undefined,
'useContext() second argument is reserved for future ' +
'use in React. Passing it is not supported. ' +
'You passed: %s.%s',
unstable_observedBits,
typeof unstable_observedBits === 'number' && Array.isArray(arguments[2])
? '\n\nDid you call array.map(useContext)? ' +
'Calling Hooks inside a loop is not supported. ' +
'Learn more at https://fb.me/rules-of-hooks'
: '',
);
// TODO: add a more generic warning for invalid values.
if ((Context: any)._context !== undefined) {
const realContext = (Context: any)._context;
@ -48,7 +61,7 @@ export function useContext<T>(
}
}
}
return dispatcher.useContext(Context, observedBits);
return dispatcher.useContext(Context, unstable_observedBits);
}
export function useState<S>(initialState: (() => S) | S) {