Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Warn for bad useEffect return value #14069

Merged
merged 1 commit into from
Nov 2, 2018

Conversation

sophiebits
Copy link
Collaborator

Mostly to catch this:

useEffect(async () => {
  // ...
  return cleanup;
});

Is this too restrictive? Not sure if you would want to do like

useEffect(() => ref.current.style.color = 'red');

which would give a false positive here. We can always relax it to only warn on Promises if people complain.

@sizebot
Copy link

sizebot commented Nov 2, 2018

ReactDOM: size: 0.0%, gzip: 0.0%

Details of bundled changes.

Comparing: 595b4f9...3f368be

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.1% +0.1% 703.61 KB 704.07 KB 162.94 KB 163.06 KB UMD_DEV
react-dom.production.min.js 0.0% 0.0% 102.41 KB 102.41 KB 33.48 KB 33.48 KB UMD_PROD
react-dom.development.js +0.1% +0.1% 698.92 KB 699.38 KB 161.57 KB 161.69 KB NODE_DEV
react-dom.production.min.js 0.0% 0.0% 102.5 KB 102.5 KB 32.99 KB 32.99 KB NODE_PROD
ReactDOM-dev.js +0.1% +0.1% 717.18 KB 717.82 KB 162.4 KB 162.54 KB FB_WWW_DEV
ReactDOM-prod.js 0.0% 0.0% 310.07 KB 310.09 KB 57.23 KB 57.23 KB FB_WWW_PROD
react-dom.profiling.min.js 0.0% 0.0% 104.87 KB 104.88 KB 33.32 KB 33.32 KB NODE_PROFILING
ReactDOM-profiling.js 0.0% 0.0% 314.4 KB 314.42 KB 58.15 KB 58.16 KB FB_WWW_PROFILING
react-dom.profiling.min.js 0.0% 0.0% 104.74 KB 104.75 KB 33.82 KB 33.82 KB UMD_PROFILING

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.1% +0.1% 490.74 KB 491.21 KB 108.68 KB 108.8 KB UMD_DEV
react-art.production.min.js 0.0% -0.1% 94.29 KB 94.3 KB 29.11 KB 29.08 KB UMD_PROD
react-art.development.js +0.1% +0.1% 422.52 KB 422.98 KB 91.66 KB 91.79 KB NODE_DEV
react-art.production.min.js 0.0% 0.0% 59.33 KB 59.34 KB 18.38 KB 18.38 KB NODE_PROD
ReactART-dev.js +0.1% +0.2% 427.12 KB 427.75 KB 90.21 KB 90.35 KB FB_WWW_DEV
ReactART-prod.js 0.0% 0.0% 183.22 KB 183.24 KB 31.37 KB 31.38 KB FB_WWW_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +0.1% +0.1% 435.16 KB 435.62 KB 94.33 KB 94.46 KB UMD_DEV
react-test-renderer.production.min.js 0.0% -0.0% 60.52 KB 60.53 KB 18.8 KB 18.8 KB UMD_PROD
react-test-renderer.development.js +0.1% +0.1% 430.38 KB 430.84 KB 93.18 KB 93.31 KB NODE_DEV
react-test-renderer.production.min.js 0.0% 0.0% 60.21 KB 60.21 KB 18.55 KB 18.55 KB NODE_PROD
ReactTestRenderer-dev.js +0.1% +0.1% 435.17 KB 435.8 KB 92.08 KB 92.21 KB FB_WWW_DEV

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +0.1% +0.1% 420.36 KB 420.83 KB 90.1 KB 90.23 KB NODE_DEV
react-reconciler.production.min.js 0.0% 0.0% 60.55 KB 60.55 KB 18.18 KB 18.18 KB NODE_PROD
react-reconciler-persistent.development.js +0.1% +0.1% 418.81 KB 419.27 KB 89.48 KB 89.6 KB NODE_DEV
react-reconciler-persistent.production.min.js 0.0% 0.0% 60.56 KB 60.56 KB 18.18 KB 18.18 KB NODE_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +0.1% +0.1% 553.4 KB 554.03 KB 121.03 KB 121.2 KB RN_FB_DEV
ReactNativeRenderer-prod.js 0.0% 0.0% 238.11 KB 238.13 KB 41.9 KB 41.9 KB RN_FB_PROD
ReactNativeRenderer-dev.js +0.1% +0.1% 553.07 KB 553.71 KB 120.93 KB 121.09 KB RN_OSS_DEV
ReactNativeRenderer-prod.js 0.0% 0.0% 238.13 KB 238.15 KB 41.9 KB 41.9 KB RN_OSS_PROD
ReactFabric-dev.js +0.1% +0.1% 543.59 KB 544.22 KB 118.57 KB 118.73 KB RN_FB_DEV
ReactFabric-prod.js 0.0% 0.0% 233 KB 233.02 KB 40.63 KB 40.63 KB RN_FB_PROD
ReactFabric-dev.js +0.1% +0.1% 543.62 KB 544.25 KB 118.59 KB 118.74 KB RN_OSS_DEV
ReactFabric-prod.js 0.0% 0.0% 233.03 KB 233.05 KB 40.64 KB 40.65 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js 0.0% 0.0% 243.9 KB 243.92 KB 43.13 KB 43.14 KB RN_OSS_PROFILING
ReactFabric-profiling.js 0.0% 0.0% 237.65 KB 237.67 KB 41.9 KB 41.91 KB RN_OSS_PROFILING
ReactNativeRenderer-profiling.js 0.0% 0.0% 243.88 KB 243.9 KB 43.13 KB 43.14 KB RN_FB_PROFILING
ReactFabric-profiling.js 0.0% 0.0% 237.61 KB 237.63 KB 41.89 KB 41.89 KB RN_FB_PROFILING

scheduler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
scheduler.development.js n/a n/a 0 B 19.17 KB 0 B 5.74 KB UMD_DEV
scheduler.production.min.js n/a n/a 0 B 3.16 KB 0 B 1.53 KB UMD_PROD

Generated by 🚫 dangerJS

Copy link
Contributor

@Jessidhia Jessidhia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confirms what I suspected all along 😇

I was especially hesitant to allow people to return Promise because I would not be able to detect that they were incorrectly returning a cleanup function from a Promise in TypeScript.

It would be nice to have some guidance as to what's the right approach when you do need to run an async task in an effect, though, other than just invoking an async IIFE inside the callback. Perhaps it's something that can be solved with ConcurrentMode (throw an async IIFE)?


I also think it's bad practice to have an implicit return when none were intended. If you're transpiling to ES5 you're going to have to pay an extra return keyword too.

getStackByFiberInDevAndProd(finishedWork),
);
}
destroy = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In production the branch if (typeof destroy !== 'function') { will still be present with destroy = null. Is it worth keeping it since the next line checks the type again?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the catch, I intended to remove the conditional from the next line.

@danielkcz
Copy link

danielkcz commented Nov 2, 2018

I would like to also know what's the recommended approach for running async code in the effect. I am so used to have async componentDidMount and similar that this really feels rather restrictive suddenly. I don't want to go back to then callback hell.

What about adding useAsyncEffect which would be expecting this and as the tradeoff it wouldn't support cleanup as in most cases the async code does not need it. Or some other way to opt-out of this warning for people who know what they are doing?

Also would be so bad to actually support Promise? I mean with all that concurrent stuff happening I would be a really surprised if React would be unable to handle asynchronous effects. In case it's planned then this warning should mention it.

@gaearon
Copy link
Collaborator

gaearon commented Nov 2, 2018

The intended solution for most async stuff is Suspense. It's unfortunate we don't have both yet so at first stages people are going to be doing useEffect more than intended. But that's mostly a transitional period.

effect.destroy = typeof destroy === 'function' ? destroy : null;
let destroy = create();
if (typeof destroy !== 'function') {
if (__DEV__ && destroy !== null && destroy !== undefined) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I kinda perfer if (__DEV__) to be a separate line for easier visual scannning

false,
'useEffect function must return a cleanup function or nothing.%s%s',
typeof destroy.then === 'function'
? ' Promises and async functions are not supported.'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add "but you can call an async function without waiting for it" or something. Or I expect we'll keep getting issues asking for this.

@danielkcz
Copy link

@gaearon

The intended solution for most async stuff is Suspense. It's unfortunate we don't have both yet so at first stages people are going to be doing useEffect more than intended. But that's mostly a transitional period.

I am still not sure what should we do at this moment? Running async function without awaiting does not make much sense to me. Can you please provide some example?

@danielkcz
Copy link

danielkcz commented Nov 2, 2018

I should probably point out that my intention is not to have useEffect wait for Promise resolution and then magically rerender. My point is that I might simply want to call something async and then just set state upon finishing it. It's basically an alternative for a missing Suspense.

function Component() {
  const [data, setData] = useState(null)
  useEffect(async () => {
    const data = await fetchData()
    setData(data)
    // in this contrieved example I could probably do it like this
    fetchData().then(setData)
    // but it's usually more complicated than that and async/await is superb sugar
  }, [])
}

I can imagine this will be much prettier with Suspense, but since we can wrap this into a custom hook then changing to suspensified solution should be fairly easy, right?

@arianon
Copy link

arianon commented Nov 2, 2018

@FredyC Maybe you could try this.

function Component() {
  const [data, setData] = useState(null)

  useEffect(() => {
	async function fetchAndDoStuffWithData() {
	  const data = await fetchData()
	  // do stuff here...
	  setData(data);
	}

    fetchAndDoStuffWithData();
  })
}

Is this too restrictive? Not sure if you would want to do like

```js
useEffect(() => ref.current.style.color = 'red');
```

which would give a false positive here. We can always relax it to only warn on Promises if people complain.
@sophiebits sophiebits merged commit 293fed8 into facebook:master Nov 2, 2018
jetoneza pushed a commit to jetoneza/react that referenced this pull request Jan 23, 2019
Mostly to catch this:

```js
useEffect(async () => {
  // ...
  return cleanup;
});
```

Is this too restrictive? Not sure if you would want to do like

```js
useEffect(() => ref.current.style.color = 'red');
```

which would give a false positive here. We can always relax it to only warn on Promises if people complain.
n8schloss pushed a commit to n8schloss/react that referenced this pull request Jan 31, 2019
Mostly to catch this:

```js
useEffect(async () => {
  // ...
  return cleanup;
});
```

Is this too restrictive? Not sure if you would want to do like

```js
useEffect(() => ref.current.style.color = 'red');
```

which would give a false positive here. We can always relax it to only warn on Promises if people complain.
@phanirithvij
Copy link

As @arianon suggested here we can use a function inside it or it can also be a self-invoking function.

const Component = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
	(async () => {
	  const data = await fetchData();
	  // do stuff here...
	  setData(data);
	})();
  });
}

@danielkcz
Copy link

I've pretty much settled with a custom hook. It has an obvious downside that I cannot use cleanup function there, but so far I haven't seen a case for an async effect that would need a cleanup. It just reads much better like this imo.

const Component = () => {
  const [data, setData] = useState(null)

  useAsyncEffect(async () => {
    const data = await fetchData();
    // do stuff here...
    setData(data);
  }, [])
}

@krzkaczor
Copy link

I created a small reusable hook, ready to copy:

import { useEffect } from "react";

/**
 * Like useEffect but works with async functions and makes sure that errors will be reported
 */
export function useAsyncEffect(effect: () => Promise<any>) {
  useEffect(() => {
    effect().catch(e => console.warn("useAsyncEffect error", e));
  });
}

@bmmpt
Copy link

bmmpt commented Jul 12, 2019

Something like this would be nicer:

export const useEffectAsync = (
    effect: () => Promise<any>,
    deps?: DependencyList
): void => {
    useEffect(() => {
        effect().catch(e =>
            // eslint-disable-next-line no-console
            console.warn('useAsyncEffect error', e)
        );
    }, deps);
};

But it throws warnings:
Line 12: React Hook useEffect was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependencies react-hooks/exhaustive-deps
Line 12: React Hook useEffect has a missing dependency: 'effect'. Either include it or remove the dependency array. If 'effect' changes too often, find the parent component that defines it and
wrap that definition in useCallback react-hooks/exhaustive-deps

@bmmpt
Copy link

bmmpt commented Jul 12, 2019

@FredyC What does your custom hook useAsyncEffect look like?

@danielkcz
Copy link

@bmmpt Do you realize those warnings are just from friendly neighbor ESLint and you can disable those per line? There is nothing else to be done here.

@bmmpt
Copy link

bmmpt commented Jul 15, 2019

It would be nice if react hooks ESLint plugin was smart enough to suppress them if they're false positives. I'm not a big fan of disabling ESLint per line, unless there really is no other option.

@danielkcz
Copy link

danielkcz commented Jul 15, 2019

@bmmpt I've never written an eslint plugin, but I can imagine it's not so easy to write heuristics that are so smart to avoid any false positives. Most importantly, you are a developer, you need to be thinking when writing code. Do not rely on a tool to do a job for you, otherwise, it might bite you. Every ESLint autofix should be validated if it makes sense.

The useEffectAsync is a custom hook you will be reusing. It's not like you will have disable comments all over the code. We are living in the world of tradeoffs, nothing is perfect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.