From dfc2cd783b81e1c0c9b963cb20363559dd0ff3f2 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Fri, 17 Apr 2026 07:47:42 +0000 Subject: [PATCH] [Fiber] Warn when useMemo is called with a non-function first argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `useMemo` received something other than a function (an object, an array, `null`, etc.) React would throw `TypeError: nextCreate is not a function` from inside `mountMemo`. The error pointed at React internals and left developers wondering what `nextCreate` was and why their object wasn't accepted (fixes #16589). Add a DEV-only warning — mirroring the existing `Expected useImperativeHandle() second argument to be a function` warning — that names the hook and the actual type received, so the developer sees a clear message pointing back at their component before the TypeError surfaces. Prod behaviour is unchanged. Co-Authored-By: Claude --- .../react-reconciler/src/ReactFiberHooks.js | 18 +++++++++++++ .../src/__tests__/ReactHooks-test.internal.js | 27 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 29c83c7d7263..ca1500ab42aa 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -2917,6 +2917,15 @@ function mountMemo( nextCreate: () => T, deps: Array | void | null, ): T { + if (__DEV__) { + if (typeof nextCreate !== 'function') { + console.error( + 'Expected useMemo() first argument to be a function that returns a value. ' + + 'Instead received: %s.', + nextCreate !== null ? typeof nextCreate : 'null', + ); + } + } const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const nextValue = nextCreate(); @@ -2936,6 +2945,15 @@ function updateMemo( nextCreate: () => T, deps: Array | void | null, ): T { + if (__DEV__) { + if (typeof nextCreate !== 'function') { + console.error( + 'Expected useMemo() first argument to be a function that returns a value. ' + + 'Instead received: %s.', + nextCreate !== null ? typeof nextCreate : 'null', + ); + } + } const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index e61e4a825602..2f8019375814 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -858,6 +858,33 @@ describe('ReactHooks', () => { ]); }); + // https://github.com/facebook/react/issues/16589 + it('warns when useMemo is called with a non-function first argument', async () => { + const {useMemo} = React; + function App() { + // $FlowExpectedError[incompatible-call] Testing runtime behaviour. + useMemo({value: 1}, []); + return null; + } + App.displayName = 'App'; + + await expect(async () => { + await act(() => { + ReactTestRenderer.create(, {unstable_isConcurrent: true}); + }); + }).rejects.toThrow('nextCreate is not a function'); + // The warning fires twice because concurrent mode retries the render + // after the TypeError thrown when `nextCreate` is invoked. + assertConsoleErrorDev([ + 'Expected useMemo() first argument to be a function that returns a value. ' + + 'Instead received: object.\n' + + ' in App (at **)', + 'Expected useMemo() first argument to be a function that returns a value. ' + + 'Instead received: object.\n' + + ' in App (at **)', + ]); + }); + // https://github.com/facebook/react/issues/14022 it('works with ReactDOMServer calls inside a component', async () => { const {useState} = React;