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);
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);
});
}, []);
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);
}, []);
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>;
}
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>;
}
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.
Top comments (0)