Skip to content

Commit

Permalink
Make all readContext() and Hook-in-a-Hook checks DEV-only (#14677)
Browse files Browse the repository at this point in the history
* Make readContext() in Hooks DEV-only warning

* Warn about readContext() during class render-phase setState()

* Warn on readContext() in SSR inside useMemo and useReducer

* Make all Hooks-in-Hooks warnings DEV-only

* Rename stashContextDependencies

* Clean up warning state on errors
  • Loading branch information
gaearon authored Jan 24, 2019
1 parent 6cb2677 commit 8bcc88f
Show file tree
Hide file tree
Showing 7 changed files with 388 additions and 129 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -419,50 +419,47 @@ describe('ReactDOMServerHooks', () => {
},
);

itThrowsWhenRendering(
'a hook inside useMemo',
async render => {
function App() {
useMemo(() => {
useState();
return 0;
});
return null;
}
return render(<App />);
},
'Hooks can only be called inside the body of a function component.',
);
itRenders('with a warning for useState inside useMemo', async render => {
function App() {
useMemo(() => {
useState();
return 0;
});
return 'hi';
}

itThrowsWhenRendering(
'a hook inside useReducer',
async render => {
function App() {
const [value, dispatch] = useReducer((state, action) => {
useRef(0);
return state;
}, 0);
dispatch('foo');
return value;
}
return render(<App />);
},
'Hooks can only be called inside the body of a function component.',
);
const domNode = await render(<App />, 1);
expect(domNode.textContent).toEqual('hi');
});

itThrowsWhenRendering(
'a hook inside useState',
async render => {
function App() {
useState(() => {
useRef(0);
return 0;
});
itRenders('with a warning for useRef inside useReducer', async render => {
function App() {
const [value, dispatch] = useReducer((state, action) => {
useRef(0);
return state + 1;
}, 0);
if (value === 0) {
dispatch();
}
return render(<App />);
},
'Hooks can only be called inside the body of a function component.',
);
return value;
}

const domNode = await render(<App />, 1);
expect(domNode.textContent).toEqual('1');
});

itRenders('with a warning for useRef inside useState', async render => {
function App() {
const [value] = useState(() => {
useRef(0);
return 0;
});
return value;
}

const domNode = await render(<App />, 1);
expect(domNode.textContent).toEqual('0');
});
});

describe('useRef', () => {
Expand Down Expand Up @@ -716,4 +713,36 @@ describe('ReactDOMServerHooks', () => {
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);

function ReadInMemo(props) {
let count = React.useMemo(() => readContext(Context), []);
return <Text text={count} />;
}

function ReadInReducer(props) {
let [count, dispatch] = React.useReducer(() => readContext(Context));
if (count !== 42) {
dispatch();
}
return <Text text={count} />;
}

const domNode1 = await render(<ReadInMemo />, 1);
expect(domNode1.textContent).toEqual('42');

const domNode2 = await render(<ReadInReducer />, 1);
expect(domNode2.textContent).toEqual('42');
});
});
});
55 changes: 45 additions & 10 deletions packages/react-dom/src/server/ReactPartialRendererHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ let renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null = null;
let numberOfReRenders: number = 0;
const RE_RENDER_LIMIT = 25;

let isInHookUserCodeInDev = false;

// In DEV, this is the name of the currently executing primitive hook
let currentHookNameInDev: ?string;

Expand All @@ -57,6 +59,14 @@ function resolveCurrentlyRenderingComponent(): Object {
currentlyRenderingComponent !== null,
'Hooks can only be called inside the body of a function component.',
);
if (__DEV__) {
warning(
!isInHookUserCodeInDev,
'Hooks can only be called inside the body of a function component. ' +
'Do not call Hooks inside other Hooks. For more information, see ' +
'https://fb.me/rules-of-hooks',
);
}
return currentlyRenderingComponent;
}

Expand Down Expand Up @@ -137,6 +147,9 @@ function createWorkInProgressHook(): Hook {

export function prepareToUseHooks(componentIdentity: Object): void {
currentlyRenderingComponent = componentIdentity;
if (__DEV__) {
isInHookUserCodeInDev = false;
}

// The following should have already been reset
// didScheduleRenderPhaseUpdate = false;
Expand Down Expand Up @@ -173,6 +186,9 @@ export function finishHooks(
numberOfReRenders = 0;
renderPhaseUpdates = null;
workInProgressHook = null;
if (__DEV__) {
isInHookUserCodeInDev = false;
}

// These were reset above
// currentlyRenderingComponent = null;
Expand All @@ -191,6 +207,15 @@ function readContext<T>(
): T {
let threadID = currentThreadID;
validateContextBounds(context, threadID);
if (__DEV__) {
warning(
!isInHookUserCodeInDev,
'Context can only be read while React is rendering. ' +
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
'In function components, you can read it directly in the function body, but not ' +
'inside Hooks like useReducer() or useMemo().',
);
}
return context[threadID];
}

Expand Down Expand Up @@ -234,7 +259,7 @@ export function useReducer<S, A>(
currentHookNameInDev = 'useReducer';
}
}
let component = (currentlyRenderingComponent = resolveCurrentlyRenderingComponent());
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
workInProgressHook = createWorkInProgressHook();
if (isReRender) {
// This is a re-render. Apply the new render phase updates to the previous
Expand All @@ -253,10 +278,13 @@ export function useReducer<S, A>(
// priority because it will always be the same as the current
// render's.
const action = update.action;
// Temporarily clear to forbid calling Hooks.
currentlyRenderingComponent = null;
if (__DEV__) {
isInHookUserCodeInDev = true;
}
newState = reducer(newState, action);
currentlyRenderingComponent = component;
if (__DEV__) {
isInHookUserCodeInDev = false;
}
update = update.next;
} while (update !== null);

Expand All @@ -267,7 +295,9 @@ export function useReducer<S, A>(
}
return [workInProgressHook.memoizedState, dispatch];
} else {
currentlyRenderingComponent = null;
if (__DEV__) {
isInHookUserCodeInDev = true;
}
if (reducer === basicStateReducer) {
// Special case for `useState`.
if (typeof initialState === 'function') {
Expand All @@ -276,7 +306,9 @@ export function useReducer<S, A>(
} else if (initialAction !== undefined && initialAction !== null) {
initialState = reducer(initialState, initialAction);
}
currentlyRenderingComponent = component;
if (__DEV__) {
isInHookUserCodeInDev = false;
}
workInProgressHook.memoizedState = initialState;
const queue: UpdateQueue<A> = (workInProgressHook.queue = {
last: null,
Expand All @@ -292,7 +324,7 @@ export function useReducer<S, A>(
}

function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
let component = (currentlyRenderingComponent = resolveCurrentlyRenderingComponent());
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
workInProgressHook = createWorkInProgressHook();

const nextDeps = deps === undefined ? null : deps;
Expand All @@ -309,10 +341,13 @@ function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
}
}

// Temporarily clear to forbid calling Hooks.
currentlyRenderingComponent = null;
if (__DEV__) {
isInHookUserCodeInDev = true;
}
const nextValue = nextCreate();
currentlyRenderingComponent = component;
if (__DEV__) {
isInHookUserCodeInDev = false;
}
workInProgressHook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
Expand Down
Loading

0 comments on commit 8bcc88f

Please sign in to comment.