admin管理员组文章数量:1332325
Getting a build error usecallback hook called conditinally. if i dont use useCallback i get the error JSX props should not use arrow functions
const LinkComp = <T extends {}>(props: LinkProps & T extends AnchorProps ? AnchorProps : ButtonProps) => {
const {
title,
hideTitle,
children,
url = '',
action,
label,
newWindow,
className,
iconName,
isExternal,
inheritColor = true,
underlineOnHover = true,
underline = false,
theme = '',
bold = false,
onClick,
modal = false,
forceAnchorTag = false,
appendQueryParams = true,
showOutline = true,
...linkProps
} = props;
const [handleClick] = useRouter(action, url);
const forwardedParams = useSelector(selectForwardedQueryParams);
const linkContent = (
<>
{iconName && <Icon name={iconName} className='mr-3' />}
{!hideTitle && (title || children)}
</>
);
if (modal) {
if (linkProps.modalTitle) delete linkProps.modalTitle;
return (
<button {...(anchorProps as ButtonProps)} role='link' onClick={onClick as ButtonProps['onClick']}>
{linkContent}
</button>
);
}
// queryString.stringifyUrl bines the params from both url and forwarded params.
// don't append the params for `tel` links
const forwardedParamsUrl = queryString.stringifyUrl({ url, query: !url?.includes('tel:') && forwardedParams });
const handleAnchorClick = useCallback((e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (handleClick) handleClick(e);
return true;
},[handleClick]);
if (forceAnchorTag) {
return (
<a href={forwardedParamsUrl} {...(anchorProps as AnchorProps)} onClick={handleAnchorClick}>
{linkContent}
</a>
);
}
// search extras uses the query params in a different way, so no need to append them here
const fullUrl = appendQueryParams ? forwardedParamsUrl : url;
return (
<Link href={fullUrl} passHref={isExternal || newWindow}>
<a {...(anchorProps as AnchorProps)} onClick={handleClick}>
{linkContent}
</a>
</Link>
);
};
export default LinkComp;
Getting a build error usecallback hook called conditinally. if i dont use useCallback i get the error JSX props should not use arrow functions
const LinkComp = <T extends {}>(props: LinkProps & T extends AnchorProps ? AnchorProps : ButtonProps) => {
const {
title,
hideTitle,
children,
url = '',
action,
label,
newWindow,
className,
iconName,
isExternal,
inheritColor = true,
underlineOnHover = true,
underline = false,
theme = '',
bold = false,
onClick,
modal = false,
forceAnchorTag = false,
appendQueryParams = true,
showOutline = true,
...linkProps
} = props;
const [handleClick] = useRouter(action, url);
const forwardedParams = useSelector(selectForwardedQueryParams);
const linkContent = (
<>
{iconName && <Icon name={iconName} className='mr-3' />}
{!hideTitle && (title || children)}
</>
);
if (modal) {
if (linkProps.modalTitle) delete linkProps.modalTitle;
return (
<button {...(anchorProps as ButtonProps)} role='link' onClick={onClick as ButtonProps['onClick']}>
{linkContent}
</button>
);
}
// queryString.stringifyUrl bines the params from both url and forwarded params.
// don't append the params for `tel` links
const forwardedParamsUrl = queryString.stringifyUrl({ url, query: !url?.includes('tel:') && forwardedParams });
const handleAnchorClick = useCallback((e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (handleClick) handleClick(e);
return true;
},[handleClick]);
if (forceAnchorTag) {
return (
<a href={forwardedParamsUrl} {...(anchorProps as AnchorProps)} onClick={handleAnchorClick}>
{linkContent}
</a>
);
}
// search extras uses the query params in a different way, so no need to append them here
const fullUrl = appendQueryParams ? forwardedParamsUrl : url;
return (
<Link href={fullUrl} passHref={isExternal || newWindow}>
<a {...(anchorProps as AnchorProps)} onClick={handleClick}>
{linkContent}
</a>
</Link>
);
};
export default LinkComp;
Share
Improve this question
edited Jan 14, 2022 at 12:18
BenM
53.2k26 gold badges115 silver badges172 bronze badges
asked Jan 13, 2022 at 20:17
kumarp0072kumarp0072
111 gold badge1 silver badge3 bronze badges
2
-
Can you please show all the code above the
useCallback
hook? As it is, that hook looks fine. – gerrod Commented Jan 13, 2022 at 21:05 -
Is your
useCallback
defined inside some conditional logic? – BenM Commented Jan 13, 2022 at 21:06
2 Answers
Reset to default 5You need to move your useCallback
definition to above the if (model)
check. This if
block may cause your ponent to render before the function has been memoized by useCallback
.
This answer is a bit late, but I find the problem quite interesting and even in late 2024, https://react.dev/reference/react/useCallback does not explain WHY the following restrictions (copied from the docs) apply:
"useCallback" is a hook, so you can only call it at the top level of your ponent or your own hooks. You cannot call it inside loops or conditions. If you need that, extract a new ponent and move the state there.
The reason why conditional calls to "useCallback" are not allowed is this: "useCallback" can be thought of as a "cache for functions", with one crucial difference from "normal" caches. Normal caches are not much more than a key/value store, where the value is usually the result of a time-consuming calculation.
An example of a time-consuming calculation could be determining the Nth prime number:
function puteNthPrime(n: number): number {
/* do whatever is needed to pute the n-th prime */
return result;
}
While this can be done quite quickly for small "n" as an argument, it bees more and more difficult with increasingly large "n"...
To avoid having to perform the calculation of the nth prime number again and again, you COULD use a cache:
const cache = new SomeCacheThingy();
function puteNthPrimeCached(n: number): number {
const cacheKey = `primeAtPosition${n}`;
if (!cache.has(cacheKey)) {
// Cache miss
cache.set(cacheKey, puteNthPrime(n));
}
return cache.get(cacheKey);
}
If "puteNthPrimeCached" is now called several times for the same "n", the calculation only takes place the first time it is called ("cache miss") and all further calls are served from the (hopefully) fast key/value store ("cache hit").
The cache key plays a crucial role here, which in the code example consists of a string that is UNIQUE for each "n":
- n=1 -> "primeAtPosition1"
- n=2 -> "primeAtPosition2"
- ...
This technique only works if a unique key is assigned to each function argument (in this case simply "n")!
Now back to "useCallback": As mentioned at the beginning, this is also a "kind of" cache. The function passed on is cached (NOT its return value!). This is not done with the aim of saving puting time, but to ensure that the same function INSTANCE is used for subsequent render calls (you can read why this is important on the page linked above).
What makes the "useCallback" cache special? It is the type of cache key. To be more precise: there is none!
(to avoid a misunderstanding: Yes, you DO have to pass a list of "dependencies" as the second argument to "useCallback". If at least one value in this list has changed since the last call to "useCallback", the previously saved version of the passed function is discarded and the currently passed version is saved and returned. But the "dependencies" do not serve as cache keys, but rather prevent a version of the function from being returned that works with stale values from the surrounding "block scope" (for "block scope" see e.g. https://www.freecodecamp/news/scope-in-javascript-global-vs-local-vs-block-scope/#heading-block-scope)).
If there is no cache key, how can the cache then decide which stored function instance to return?
The answer is: via the call order of "useCallback"! If, for example, you cache 2 functions using "useCallback", you will receive the two previously cached functions in the order in which they were initially cached in subsequent render calls. At this point it should be clear why conditional calls to "useCallback" inevitably lead to a big mess.
Consider this small React ponent which has a boolean "flag" state that is toggled on each mouse click:
export function Sample() {
const [flag, setFlag] = React.useState(true);
if (flag) {
const funcA = React.useCallback(() => ['I am A', flag], []);
console.log('funcA result:', funcA());
} else {
const funcB = React.useCallback(() => ['I am B', flag], []);
console.log('funcB result:', funcB());
}
return (
<span onClick={() => setFlag(!flag)}>Click me</span>
);
}
The log should look as follows:
// 1. render: flag: true / funcA result: (2) ['I am A', true]
// 2. render: flag: false / funcB result: (2) ['I am A', true]
// 3. render: flag: true / funcA result: (2) ['I am A', true]
// 4. render: flag: false / funcB result: (2) ['I am A', true]
There are 2 interesting findings:
- We always get the function result of funcA() - even in render 2 and 4 where the code takes the "else" branch
- While the "flag" toggles as expected from "true" to "false" and back again, the function return value remains "[..., true]"
The first finding is directly related to calling "useCallback" conditionally: In the first render it is called with "funcA" and this function is stored as the first to be returned on subsequent renders. Consequently, it is returned in the second render, even though "funcB" was passed. Remember: It's the ORDER of calling "useCallback" and it is only ever called once per render. So, it associated "funcA" with the first call and will return that over and over again.
The second finding nicely illustrates what the "dependencies" list is needed for: When "funcA" was initially stored in the cache, the block scoped variable "flag" pointed to "true" and thus on subsequent calls to "useCallback" you will get that function instance back (you could also consider it a snapshot of the block scoped variable "flag" at the time of taking it).
In order to overe the second problem we need to pass "flag" as a dependency:
export function Experimental() {
const [flag, setFlag] = React.useState(true);
if (flag) {
const funcA = React.useCallback(() => ['I am A', flag], [flag]);
console.log('flag:', flag, '/ funcA result:', funcA());
} else {
const funcB = React.useCallback(() => ['I am B', flag], [flag]);
console.log('flag:', flag, '/ funcB result:', funcB());
}
return (
<span onClick={() => setFlag(!flag)}>Click me</span>
);
}
If you look at your console log everything seems to be fine now. But this is actually only, because in each render the "flag" has toggled and thus destroying the previously stored function instance. In this case, you could also do without "useCallback". The result would be exactly the same.
本文标签:
版权声明:本文标题:javascript - Error: React Hook "useCallback" is called conditionally. React Hooks must be called in the exact 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1742271682a2444424.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论