DEV Community

Cover image for A Story Of React Effects.
MirAli Mobasheri
MirAli Mobasheri

Posted on

A Story Of React Effects.

We use states to update what components render. But what if some operations don’t cause immediate changes in the component’s output?

The React documentation calls them side effects. And some examples include using web APIs, handling HTTP requests, and attaching listeners to events.

For handling these, React provides us with the useEffect hook. It calls the function you pass it each time the component renders or updates. Through this article, we’ll learn why React implements this functionality.

And when you finish reading this you can start to use effects to your profit!


Side effects

Every action ends in an outcome. And every component renders an output. To lose weight, you need to burn fat. It’s possible by going on a diet and beginning exercises.

But get ready for the side effects.

These side effects include headaches, fatigue and, dizziness. They are so because they are not what you expect. Neither are they a part of the main routine.

When exercising exhausts you, you need to rest for a while. You need to gain energy before you can return to your everyday life.

In a React app, when requesting data from the server, we can face the same problem. Because we're not sure how long it can take to retrieve the data from the server. And we're not sure if we can render it in the component's output.

So we call the useEffect hook to start fetching the needed data from the server. In the meanwhile, the component renders a placeholder output. You know what it is.

A beautiful loading indicator!


The useEffect Hook

From the React documentation:

By using this Hook, you tell React that your component needs to do something after render. React will remember the function you passed (we’ll refer to it as our “effect”), and call it later after performing the DOM updates.

So you pass a function to useEffect, and it remembers to call it after each render or update. This means that by the time the component calls your effect, it has already updated the state. And the browser has finished painting the screen.

But why do we need to wait for the render to finish before running the effect?

We can find the answer.


The variety of side effect

Think of a big drapery shop. Full of luxurious and colorful textiles. Rows over rows of fabrics. Silks from all over the world. A great show of human creativity.

Now imagine the customers. A market welcomed by women. Waves of them coming to the store and leaving hourly. Think of how the owners can manage the purchases and the sales.

Then there's one special case. One of the customers, a young blonde lady, is looking for a rare type of silk.

It was in stock three days ago. But now, all of it was sold out.

So the salesman takes the lady's phone number. Then he makes phone calls with some well-known silk traders to order a new set of that special silk.

In the end, he informs the lady that he'll call her later as soon as the product is available. (And let's hope he doesn't text her: "How you doin?"!)

A drapery shop could run out of any of its products at any time. Do you think the manager should not open his store if some of his assets are missing?

Does the above question make any sense to you? By waiting for the perfect time, he may not as well make more than ten sales per week.

A React component is like this drapery shop. It renders what it has in stock. And if data isn't currently available in the state, it renders a loading indicator. Then tries to fetch the data in a useEffect hook.

A code example looks as following:

const Drapery = (props) => {
  const [isLoading, setIsLoading] = useState(true)
  const [data, setData] = useState(null)

  useEffect(() => {
    fetch("https://example.com/endpoint")
      .then(res => res.json())
      .then(data => {
        setData(data)
        setIsLoading(false)
      })
      .catch(error => console.log("There was an error while fetching data", error)
  }, [])

  return isLoading ?
    <Spinner /> :
    <ListComponent data={data} />
}
Enter fullscreen mode Exit fullscreen mode

This code renders a component with two states.

One of them is loading. In case its value is true, it means data isn't available. So the component should render a loading indicator. The other one is data which holds a list of drapery items. And ListComponent is going to render them.

We should focus on the useEffect part. The hook accepts two arguments; a function and a dependency array.

The function holds the logic of your effect.

Using the dependency array, you pass a set of values that React will start to track. Anytime any of the values in this array changes, React calls your effect.

In this case, we've passed an empty array as a dependency. This indicates that React calls this effect only once. That is after the initial render.

If you pass no dependency array to the hook, React calls the effect after each render or update.

The function we passed to useEffect tries to start fetching data from the server. JS fetch API returns a promise. We use it to update the states as soon as we get the response.

We update the data state with what we get from the server. Then we set the loading state to false. Then the condition in our component's return expression evaluates to false. That’s when it renders the data using ListComponent.

While our effect causes a re-render by updating states, it doesn't affect the render. It runs in parallel. And it never causes any direct changes in what the component renders.

In the previous section we had a question. Why should effects run after the render is updated?

In this case, a good reason is that network calls can affect the client's performance. We need to show the loading indicator as soon as possible.

In web apps, each second lost due to performance is thousands of potential users lost forever:(


How much did we make today?

A drapery shop's manager needs to keep its daily sales data in a secure place. He needs to have a record of everything they buy and sell daily.

They save pieces of information on every new asset they deliver and sell daily. They should do it as soon as any change occurs.

While writing React web apps we sometimes need to save the user's data in the client's storage. The localStorage API is a good option for this case.

We can use the useEffect hook to listen to changes in state as soon as it is updated. Then we can set a JSON clone of the new state in the client's storage.

An example:

const UserContext = createContext()

const UserProvider = ({children}) => {
  const [user, setUser] = useState(null)

  useEffect(() => {
    const persistedUserData = localStorage.getItem("user")
    if(persistedUserData) setUser(JSON.parse(persistedUserData))
  }, [])

  useEffect(() => {
    const jsonUserData = JSON.stringify(user)
    localStorage.setItem("user", jsonUserData)
  }, [user])

  return (
    <UserContext.Provider value={{user}}>
      {children}
    </UserContext.Provider>
  )
}
Enter fullscreen mode Exit fullscreen mode

In the above example, we are using React.createContext to initialize a context. If you don't know about context, you can think of it as an API. It makes the value you pass to its Provider available to every component in its tree. To learn more you can read the documentation.

Then in the body of the Provider, we have used three hooks. One is a useState which holds user info. Its initial value is null, which means no user data is available.

The two other hooks are both useEffects. The first effect runs only after the first render, as its dependency array is empty. We use it to check if there's any user data previously stored in the client's storage. If so, we parse the JSON data as a JS object and update our state with it.

The second hook has a dependency array with one element: user state value. So it will run every time we update the user state. This effect makes a JSON copy of the user state and saves it to the client's localStorage.

The second useEffect is a good example of when we should use effects. We update the client's localStorage, which causes no change in the render. There's no place for its logic in the component body.

We've also learned how we can control when effects are run by setting their dependency arrays.

Good!


Did a fire catch here?

Silks, clothes, curtains. They're all so soft and lovely. Yet they're so vulnerable to ignition. A small flame can turn the whole drapery into ashes.

Piles of money, into ash, that is.

A drapery shop's manager is aware of this hazard and takes every precaution step needed to prevent its occurrence. But there's always a chance of a mistake. And a mistake is enough to undo all the cautions.

So he has specified an explicit procedure for when fire catches in the store. And he has instructed it to his employees. In addition, he has printed them on paper and stuck them on the wall.

There's little chance of things going wrong, but it might happen at any time. And he needs to bear it in mind.

Do you need a similar web app example? A text editor is one.

When working with a text editor, you need to save your changes to not lose them when you reopen the text. Now there's a chance you forget you haven't saved your changes and click the browser's close button.

A good web application is going to prevent this disaster happen. It stops the navigator from closing the tab and alerts you that you haven’t saved your work.

Sir or madam, truly your forgiveness I implore, are you sure you want to continue?

To implement this feature in a React app we can do it with useEffect like the example below:

  const Editor = (props) => {
    useEffect(() => {
      window.addEventListener("beforeunload", showDialog)

    return () => window.removeEventListener("beforeunload")
    }, [])

    return <EditorPanel/>
  }
Enter fullscreen mode Exit fullscreen mode

In the above example, we attach a listener to window's beforeunload event. This event is fired whenever the user tries to close the browser or the current tab.

The useEffect hook is a good choice in this case because the component is already rendered. Then we're sure that the window object is loaded and we can attach an event listener to it.

Also, we've used a return statement in the useEffect hook. You can always return a function in your effect. React will call this function when the component is unmounted.

We returned a function that removes the event listener attached to the window because we don't want to prevent the user from exiting the app when there is no editor open.

A function you return in your effects is called cleanup function. Since effects like this listener are out of the component's scope and are not related to its render, it's always a good idea to write a cleanup function for them.

When a drapery shop's manager goes broke and has to leave his place, he also has to clean the wall from his emergency instructions. The new manager won't like to see them there!


That's it!

In this article, we learned why we need to separate effect logic from render and how to do it wisely. We also learned different use cases of this feature and how to handle them correctly.

I hope you liked it.

One last word. If you were in doubt whether you should write an effect or just bring the logic inside the component's body, ask yourself this question: Is the operation immediately going to change what the component renders?


In case this article was of any help to you, I will be glad if you follow me on Twitter or mention me in an appreciation tweet. That means a lot to me!
@MobasheriMirali

Discussion (0)