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
Add a ment  | 

2 Answers 2

Reset to default 5

You 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:

  1. We always get the function result of funcA() - even in render 2 and 4 where the code takes the "else" branch
  2. 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.

本文标签: