DEV Community

Cover image for ReactJS: Things to consider before using Closures in ReactJS
Labeeb Ahmad
Labeeb Ahmad

Posted on

ReactJS: Things to consider before using Closures in ReactJS

Closures can 'break' reactivity if misused - Let's have a look.

In JS, closure is a function that "remembers" all variables from its parent scope.

For example, following closure will be executed after 1000ms. And it will have access to date variable that is not available inside the callback passed to setInterval. This is how a closure "remembers" variables from its parent scope.

var date = new Date();
setInterval(() => {
    console.log(date);
}, 1000);
Enter fullscreen mode Exit fullscreen mode

But if this date variable is itself reactive & keeps on changing then closure will not be able to keep track of changing value of date variable.

I found out this behavior while I was developing a socket based chat app. Chat app mainly consists of functional components so I was registering socket io event listeners inside useEffect() as follows:

  const [time, setTime] = useState(new Date());
  useEffect(() => {
    socket.emit("new_message", function (newMessageData) {
      console.log("time", time);
    });
  }, []);
Enter fullscreen mode Exit fullscreen mode

here time is a variable that is reactive and its value is changed every 900ms using following useEffect hook:

  useEffect(() => {
    /*** updates time every 1000ms ***/
    setInterval(() => {
      setTime(new Date());
    }, 1000);
  }, []);
Enter fullscreen mode Exit fullscreen mode

After combining above code into a single component App.js, my code looks like this:

import "./styles.css";
import React, { useState, useEffect } from "react";
/***** 
following socket object is to mimic 
socket client for the sake of example 
***/
const socket = {
  on: function (type, cb) {
    setInterval(() => {
      cb();
    }, 1000);
  }
};
/****** ***/

export default function App() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    setInterval(() => {
      setTime(new Date());
    }, 1000);
  }, []);

  useEffect(() => {
    socket.on("new_message", function () {
      console.log("time", time);
    });
  }, []);
  return <div className="App">{time.toString()}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Problem is new_message, closure doesn't keep track of changes that are being made to time variable & this is because closure only remembers first value of time variable.

Have a look at this https://storyxpress.co/video/koigqbiphyw2b0v9b
value of time variable doesn't change in logs as this is coming from new_message handler through a closure.

However we can fix it like this:

import "./styles.css";
import React, { useState, useEffect } from "react";
const socket = {
  on: function (type, cb) {
    setInterval(() => {
      let newMessageData = new Date().getTime();
      cb(newMessageData);
    }, 1000);
  }
};
export default function App() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    setInterval(() => {
      setTime(new Date());
    }, 1000);
  }, []);

  const [newMessageData, setNewMessageData] = useState(new Date());
  useEffect(() => {
    socket.on("new_message", function (data) {
      setNewMessageData(data);
    });
  }, []);

  useEffect(() => {
    console.log("time.....", time);
  }, [newMessageData]);

  return <div className="App">{time.toString()}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Now instead of directly accessing time variable inside closure I am just setting value newMessageData using a hook.

Then I attached another useEffect() hooks that keeps track of changes to newMessageData and fetches an updated time variable whenever newMessageData changes.

Have a look at this screencast https://storyxpress.co/video/koigvz8sd5tdeeg0c : time variable is updated as it is logged to console every 1000ms.

Latest comments (0)