useState not updating correctly with socket.io - reactjs

I am pulling data via socket.io but when listening for incoming messages from the server my state is not updating correctly.
I can see the data is being pulled correctly from the socket (50 reqs peer second) but setQuotes just replaces the existing item with the new item returned from the server (so my state always has a length of one).
const [quotes, setQuotes] = useState([])
const subscribe = () => {
let getQuote = 'quote.subscribe';
socket.emit(getQuote, {});
socket.on('listenAction', function (msg) {
setQuotes([...quotes, msg]) // This just replace the entire state instead of adding items to the existing array
});
}
Subscribe // Open web socket stream

You need to move the listener outside of the subscribe function.
Since it is a sideEffect you should wrap it in React.useEffect
It's also a good idea to use functional setstate to read previous values.
React.useEffect(() => {
socket.on('listenAction', function (msg) {
setQuotes((quotes) => [...quotes, msg])
});
}, []);

In your example you use setState like this
setQuotes([...quotes, msg])
And every time you get message, javascript engine tries to find "quotes" variable in scope of this function
function (msg) {
It can not find it here and move to the scope, where this function was defined ( scope of component function).
For every function call, there is new scope. And react calls component function for each render. So, your function with "msg" in arguments, where you use setQuotes, every time uses "quotes" state from first render.
In first render you have an empty array.
Then you have [...firstEmptyArray, firstMessage]
Then you have [...firstEmptyArray, secondMessage]
Then you have [...firstEmptyArray, thirdMessage].
Probably, you can fix it if you will use setQuotes like;
setQuotes(oldQuotes => [...oldQuotes, msg]);
In this way it will use previousValue for calclulating new one.
PS. Be aware of not to call your subscribe function directly in each render and put it in useEffect with correct dependency array as second argument.

You can use the concat func over here
setQuotes(prevState => prevState.concat(msg))

Related

Infinite loop in useEffect fix?

I am currently pulling data from a firebase realtime database and the data will initial populate. When I refresh the page though, the contents disappear. I added an empty array at the end of the UseEffect() to stop the infinite loop issue that we were having, but it seems to stop updating our array when refreshed.
useEffect(() => {
let jobs = [];
firebase.database().ref("1h5GOL1WIfNEOtcxJVFQ0x_bgJxsPN5zJgVJOePmgJOY/Jobs").on("value", snapshot => {
snapshot.forEach(snap => {
jobs.push(snap.val());
});
})
populateJobs(jobs);
},[]);
As ray commented, it does matter how populateJobs is defined. But at a first guess, you'll need to call that from inside the callback:
useEffect(() => {
firebase.database().ref("1h5GOL1WIfNEOtcxJVFQ0x_bgJxsPN5zJgVJOePmgJOY/Jobs").on("value", snapshot => {
let jobs = [];
snapshot.forEach(snap => {
jobs.push(snap.val());
});
populateJobs(jobs);
})
},[]);
I assume populateJobs is function declared in your scope.
If so, you may want to wrap it in useCallback to ensure the function reference doesn't change.
const populateJobsFixed= useCallback(populateJobs, [])
useEffect(() => {
...
populateJobsFixed(jobs);
},[populateJobsFixed]);
populateJobs is a dependency of the useEffect
You need to have the dependency list be
},[populateJobs]);
Instead of an empty array
The second argument of useEffect(()=>{},[]) which takes an array of argument tells react on what changes your callback passes there should run.
If passed an empty array it runs only once, behaving like componentdidmount method
When a variable is passed, it runs every time the value of the variable is changed.
Also if we pass an object as second parameter it will check for the reference change too.

React JS component declare a variable required to assign value on useEffect to use across multiple functions

In a React Component, Need to declare variable to access across multiple functions. Two approaches tried to achieve this - useState (less good because of rendering) or let/var. Example:
const [userName, setuserName] = useState("");
let root = "";
problem (part 1): on useEffect after setting setuserName hook I can not access userName immediately. but after modifying root I can use the variable immediately. Example:
useEffect(() => {
//get name from firebase doc ref...
let name = user.claims.userName; //"AName"
setuserName(name);
root = name ;
console.log("userName: " + userName+ " , " + root); //here userName is empty but root has the name.
return () => {
// db.ref("").off("value", listener);
};
}, []);
problem (part 2): if both used in a function declared in the component problem part 1 reverses, I mean, the userName (hook) will have name in it but the root variable will be empty. Example:
async function handleSubmit(e) {
// e.preventDefault();
console.log("root = " + root + " , userName : " + userName );//here root is empty but userName has the name.
return; //or call methods to firebase ref.
}
Tested with assigning variables and state with string literals from code within (Eg: root = "testName"). Just need a temporary variable to call firebase functions. Why this behaviour happens and what approach should I use here?
Update:
Re rendering of component will reset the variable, using state hook is kinda overkill but only option.
Now to set the value of state (userName), useEffect is used in the first place. It also calls and fetches other variables from firebase based on the state (userName). If you dont have that reference nothing else works. You dont have that reference because useState will have it on next render, so not usable right here. You can store userName on a variable. Again render will reset that variable, so wont work with variable either. Hope this make sense.
Now I have solved this, thought it would be interesting to put on SO.
first issue is related to the fact the setState doesn't update state synchronous, which is a common doubt, hence console.log will not print next state. And you can't await on some state fwiw, since it's not an actually promise. If you need to perform some action based on a state change you should create another use useEffect with that state as dependency:
useEffect(() => {
//get name from firebase doc ref...
let name = user.claims.userName; //"AName"
setuserName(name);
return () => {
// db.ref("").off("value", listener);
};
}, []);
useEffect(() => {
// another useEffect based on userName as dependency
//do something on userName update...
console.log(userName);
}, [userName]);
second issue is that on rerenders only the state value is reflected correctly. Once setuserName is resolved and your component is rerendered, any declared variables like root will be recreated based on its original logic (unless you memoize it with useMemo to avoid some expensive calculation).
you should better use state across places/functions to control the logic of your component, not variables declared at your component body which would lead to unexpected behaviors.

useEffect not triggering when object property in dependence array

I have a context/provider that has a websocket as a state variable. Once the socket is initialized, the onMessage callback is set. The callback is something as follows:
const wsOnMessage = (message: any) => {
const data = JSON.parse(message.data);
setProgress(merge(progress, data.progress));
};
Then in the component I have something like this:
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress[pvc.metadata.uid]])
return (
{/* stuff */}
);
}
However, the effect isn't triggering when the progress variable gets updated.
The data structure of the progress variable is something like
{
"uid-here": 0.25,
"another-uid-here": 0.72,
...etc,
}
How can I get the useEffect to trigger when the property that matches pvc.metadata.uid gets updated?
Or, how can I get the component to re-render when that value gets updated?
Quoting the docs:
The function passed to useEffect will run after the render is
committed to the screen.
And that's the key part (that many seem to miss): one uses dependency list supplied to useEffect to limit its invokations, but not to set up some conditions extra to that 'after the render is committed'.
In other words, if your component is not considered updated by React, useEffect hooks just won't be called!
Now, it's not clear from your question how exactly your context (progress) looks like, but this line:
setProgress(merge(progress, data.progress));
... is highly suspicious.
See, for React to track the change in object the reference of this object should change. Now, there's a big chance setProgress just assignes value (passed as its parameter) to a variable, and doesn't do any cloning, shallow or deep.
Yet if merge in your code is similar to lodash.merge (and, again, there's a huge chance it actually is lodash.merge; JS ecosystem is not that big these days), it doesn't return a new object; instead it reassigns values from data.progress to progress and returns the latter.
It's pretty easy to check: replace the aforementioned line with...
setProgress({ ...merge(progress, data.progress) });
Now, in this case a new object will be created and its value will be passed to setProgress. I strongly suggest moving this cloning inside setProgress though; sure, you can do some checks there whether or not you should actually force value update, but even without those checks it should be performant enough.
There seems to be no problem... are you sure pvc.metadata.uid key is in the progress object?
another point: move that dependency into a separate variable after that, put it in the dependency array.
Spread operator create a new reference, so it will trigger the render
let updated = {...property};
updated[propertyname] =value;
setProperty(()=>updated);
If you use only the below code snippet, it will not re-render
let updated = property; //here property is the base object
updated[propertyname] = value;
setProperty(()=>updated);
Try [progress['pvc.metadata.uid']]
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress['pvc.metadata.uid']])
return (
{/* stuff */}
);
}

React Hooks API call - does it have to be inside useEffect?

I'm learning React (with hooks) and wanted to ask if every single API call we make has to be inside the useEffect hook?
In my test app I have a working pattern that goes like this: I set the state, then after a button click I run a function that sends a get request to my API and in the .then block appends the received data to the state.
I also have a useEffect hook that runs only when the said state changes (using a dependency array with the state value) and it sets ANOTHER piece of state using the new data in the previous state. That second piece of state is what my app renders in the render block.
This way my data fetching actually takes place in a function run on a button click and not in the useEffect itself. It seems to be working.
Is this a valid pattern? Thanks in advance!
Edit: example, this is the function run on the click of the button
const addClock = timezone => {
let duplicate = false;
selectedTimezones.forEach(item => {
if (item.timezone === timezone) {
alert("Timezone already selected");
duplicate = true;
return;
}
});
if (duplicate) {
return;
}
let currentURL = `http://worldtimeapi.org/api/timezone/${timezone}`;
fetch(currentURL)
.then(blob=>blob.json())
.then(data => {
setSelectedTimezones(prevState => [...prevState, data]);
}
);
}
Yes, apis calls that happen on an action like button click will not be part of useEffect call. It will be part of your event handler function.
When you call useEffect, you’re telling React to run your “effect”
function after flushing changes to the DOM
useEffect contains logic which we would like to run after React has updated the DOM. So, by default useEffect runs both after the first render and after every update.
Note: You should always write async logic inside useEffect if it is not invoked by an event handler function.
Yes, you can make api requests in an event handler such as onClick.
What you don't want to do is make a request directly inside your functional component (since it will run on every render). As long as the request is inside another function and you only call that function when you actually want to make a request, there is no problem.

How does React Hooks useCallback "freezes" the closure?

I'd like to know how does React "freezes" the closure while using the useCallback hook (and with others as well), and then only updates variables used inside the hook when you pass them into the inputs parameter.
I understand that the "freeze" may not be very clear, so I created a REPL.it that shows what I mean: https://repl.it/repls/RudeMintcreamShoutcast. Once you open the code, open your web browser console and start clicking on the count button.
How come the value outside compared to the one inside, for the same variable, is different, if they're under the same closure and referencing the same thing? I'm not familiar with React codebase and so I suppose I'm missing an under the hood implementation detail here, but I tried to think how that could work for several minutes but couldn't come up with a good understanding on how React is achieving that.
The first time the component is rendered, the useCallback hook will take the function that is passed as its argument and stores it behind the scenes. When you call the callback, it will call your function. So far, so good.
The second time that the component is rendered, the useCallback hook will check the dependencies you passed in. If they have not changed, the function you pass in is totally ignored! When you call the callback, it will call the function you passed in on the first render, which still references the same values from that point in time. This has nothing to do with the values you passed in as dependencies - it's just normal JavaScript closures!
When the dependencies change, the useCallback hook will take the function you pass in and replace the function it has stored. When you call the callback, it will call the new version of the function.
So in other words, there's no "frozen"/conditionally updated variables - it's just storing a function and then re-using it, nothing more fancy than that :)
EDIT: Here's an example that demonstrates what's going on in pure JavaScript:
// React has some component-local storage that it tracks behind the scenes.
// useState and useCallback both hook into this.
//
// Imagine there's a 'storage' variable for every instance of your
// component.
const storage = {};
function useState(init) {
if (storage.data === undefined) {
storage.data = init;
}
return [storage.data, (value) => storage.data = value];
}
function useCallback(fn) {
// The real version would check dependencies here, but since our callback
// should only update on the first render, this will suffice.
if (storage.callback === undefined) {
storage.callback = fn;
}
return storage.callback;
}
function MyComponent() {
const [data, setData] = useState(0);
const callback = useCallback(() => data);
// Rather than outputting DOM, we'll just log.
console.log("data:", data);
console.log("callback:", callback());
return {
increase: () => setData(data + 1)
}
}
let instance = MyComponent(); // Let's 'render' our component...
instance.increase(); // This would trigger a re-render, so we call our component again...
instance = MyComponent();
instance.increase(); // and again...
instance = MyComponent();
I came here with a similar, rather vague uncertainty about the way useCallback works, its interaction with closures, and the way they are "frozen" by it. I'd like to expand a bit on the accepted answer by proposing to look at the following setup, which shows the working of useCallback (the important aspect is to ignore the linter's warning, for pedagogical reasons):
function App() {
const [a, setA] = useState(0)
const incrementWithUseCallback = useCallback(() => {
// As it closes on the first time `App` is called, the closure is "frozen" in an environment where a=0, forever
console.log(a)
setA(a + 1)
}, []) // but.. the linter should complain about this, saying that `a` should be included!
const incrementWithoutUseCallback = () => {
// This will see every value of a, as a new closure is created at every render (i.e. every time `App` is called)
console.log(a)
setA(a + 1)
}
return (
<div>
<button onClick={incrementWithUseCallback}>Increment with useCallback</button>
<button onClick={incrementWithoutUseCallback}>Increment without useCallback</button>
</div>
)
}
So we clearly see that useCallback effectively "freezes" its closure at a certain moment in time, which is a concept that must be understood clearly, in order to avoid confusing problems, which are sometimes also referred as "stale closures". This article probably does a better job of explaining it than me: https://tkdodo.eu/blog/hooks-dependencies-and-stale-closures
Here's a slightly another view on example code provided by Joe Clay, which emphasizes closure context in which callback is called.
//internal store for states and callbacks
let Store = { data: "+", callback: null };
function functionalComponent(uniqClosureName) {
const data = Store.data;//save value from store to closure variable
const callback = Store.callback = Store.callback || (() => {
console.log('Callback executed in ' + uniqClosureName + ' context');
return data;
});
console.log("data:", data, "callback():", callback());
return {
increase: () => Store.data = Store.data + "+"
}
}
let instance = functionalComponent('First render');
instance.increase();
instance = functionalComponent('Second render');
instance.increase();
instance = functionalComponent('Third render');
As you see, callback without dependencies will be always executed in the closure where it was memorized by useCallback, thus 'freezing' closure.
It happens because when function for callback is created, it is created only once, during first 'render'. Later this function is re-used, and use value of data which was recorded from Store.data during first call.
In the next example you can see the closure 'freezing' logic "in essence".
let globalX = 1;
const f = (() => {
let localX = globalX; return () => console.log(localX); }
)();
globalX = 2;//does not affect localX, it is already saved in the closure
f();//prints 1

Resources