You are using `useEffect` the wrong way 🤯
Tags: react, nextjs, programming, craftsmanship
Published: 2024-02-27 13:05:35

This article won't describe the API of the React hook `useEffect`, so refer to the [documentation](https://react.dev/reference/react/useEffec) or other articles. 

Very often while reviewing PRs, I see developers using the React hook `useEffect` the wrong way. Behind these errors, multiple reasons coexist. They reduce the performances and even worst, reduce the readability of the code. I know that some of these situations are even visible on the React website but you should avoid using them.

These are few examples:

## To add side-effect when some data changed

Developers find smart to move side-effects in dedicated components to simplify the logic. 


```tsx
function Component({ value }) {

  useEffect(() => {
    saveValueOnServer(value)
  }, [value])
}

function Parent() {
  const [value, setValue] = useState()

  return (
    <>
      <Component value={value} />
      <input value={value} onChange={event => setValue(event.target.value)} />
    </>
  )
}
```

This pattern is inspired by the event-driven interface design from some UI framework like Qt in C++ for example. But it is not the way in react.

- `useEffect` requires a variable change so you might be inclined to create artificial variable to trigger this side-effect
- a event system already exists in javascript so you should this one instead
- when you are new in the project, it is very hard to track where the side-effects are triggered. Debugging will be harder. It becomes even **impossible** when you start using data from a data system management like redux.
	

### Solution

When you want to add a side-effect to an operation, add it directly after the operation. It will easier to debug in the future.

```ts 
function Parent() {
  const [value, setValue] = useState()

  const handleChange = (event) => {
	const newValue = event.target.value
	setValue(newValue)
	saveValueOnServer(newValue) // <-- the code just after will be easier to debug
  }

  return (
    <input value={value} onChange={event => setValue(event.target.value)} />
  )
}
```

## To memoize data
Developers use sometimes `useEffect` to memoize data:

```tsx
function Component({ value }) {
  const [computedValue, setComputedValue] = useState()

  useEffect(() => {
    setComputedValue(computeValue(value))   
  }, [value])

  return <>{computedValue}</>
}
```

This is a wrong way to memoize value for 3 reasons:
- it requires more code than `useMemo` 
- `useEffect` is executed after the render so it means the value will always be displayed with a short time lag (very short but sill existing) 
- the combo `useEffect + useState` forces 2 renders instead of one

### The solution

Use `useMemo` which is dedicated for this usage:

```tsx
function Component({ value }) {
  const computedValue = useMemo(() => computeValue(value), [value])

  return <>{computedValue}</>
}
```

> The only reason you would use `useEffect` for memoization is if the computation of the value is really long and you don't want to slow down the render and prefer to run the computation in the background.

## To fetch data

This one is definitely the most frequent Dark pattern and even tutorial suggest this approach:

```tsx
function Component() {
  const [data, setData] = useState()
  
  useEffect(() => {
     const fetch = async () => {
        setData(await fetchData())
     }

     fetch().then(() => {}).catch(() => {})
  }, [])

  return <>{data}</>
}
```

This pattern is really underperforming because:
- it starts fetching data after the first render instead of before
- fetching data is asynchronous while `useEffect` callback is synchronous so developers have to use workarounds and it makes the code messy.

### Solution

I suggest to use the hook `useQuery` from `@tanstack/react-query`

If you don't want an extra library just for this hook, you can use a custom hook like:

```tsx
const useOnce = (callback: () => Promise<void>) => {
  const executed = useRef(false)
 
  if (!executed.current) {
    callback().then(() =>{}).catch(() => {})
    executed.current = true
  }
}

function Component() {
  const [data, setData] = useState()
  
  useOnce(async () => {
     setData(await fetchData())
  })

  return <>{data}</>
}
```


## The most important reason

`useEffect` gives a lot of flexibility. But this flexibility is also a doom since as a developer, you won't be understand the goal of this hook in a blink. You will need to dig deeper than with a `useMemo` or a `useQuery`.

So please prefer the dedicated hooks to increase the readability of the code.

## When you have the right to use `useEffect`

The only right reason to use `useEffect` is to attach a listener to a DOM element. Of course you also have some edge cases. However from my experience, 80% of the time, developers use the hook `useEffect` for situations that are not edge cases. So if you are not sure, don't use it.


```tsx
function Component() {
  const container = useRef()

  useEffect(() => {
    const onClick = () => {
      // do something
    }

    container.current.addEventListener(onClick)

    return () => container.current.removeEventListner(onClick)
  },[])

  return (
    <div ref={container}>
    </div>
  )
}
```

So please keep your code more readable and use the hook `useEffect` less often. All your teammates will thank you.