useEffect for real time data update - javascript

I am creating an easy chat app, with different text channels. I am facing an infinite loop issue when using the useEffect hook to update the messagesList on real time. You can see the code below but this is what I am trying to achieve:
First useEffect is for the chat window to scroll to the last message every time there is a change in the messagesList array. This means: I am in the middle of the messages window, I write a new message and it takes me to the bottom. This is working fine.
Second useEffect is for the messagesList to be rendered whenever the channel is changed or there is any change in the messagesList. Adding the messagesList as a dependency is causing the infinite loop... but I think I need it cause otherwise the following happens: user1 is inside the chat channel and user2 writes a new message. User1 wont see the new message displayed as his chat is not being re-rendered. How would you make it for the new message to be displayed for user1?
Sorry for the confusing question and thanks a lot in advance!
useEffect(() => {
anchor.current.scrollIntoView(false);
}, [messagesList]);
useEffect(() => {
const unsubscribe = onSnapshot(
collection(firestore, `channels/${activChannel}/messages`),
(snapshot) => {
console.log(snapshot.docs);
getMessagesList();
}
);
return () => unsubscribe();
}, [activChannel, messagesList]);

I am not familiar with firestore, but perhaps you could tie the updating of the messages to the event that an user submits his message or use useSyncExternalStore. This piece of documentation on useEffect use cases might help you.
an excerpt from the docs:
Here, the component subscribes to an external data store (in this
case, the browser navigator.onLine API). Since this API does not exist
on the server (so it can’t be used to generate the initial HTML),
initially the state is set to true. Whenever the value of that data
store changes in the browser, the component updates its state.
Although it’s common to use Effects for this, React has a
purpose-built Hook for subscribing to an external store that is
preferred instead. Delete the Effect and replace it with a call to
useSyncExternalStore:

Related

React force update

I always run into situations where I need to force rerender, while I'm still in the execution of some function, so I developed my solution to this and I need to know if this is right or there is a simpler way to achieve the same goal.
I rely on the state variable my_force_update, then I change it to a random value when I want to enforce a change. like:
const [my_force_update, setMyForceUpdate] = useState(0);
useEffect(()=>{}, [my_force_update]);
const handleSubmit = async () =>{
await prm1();
stMyForceUpdate(Math.random()); // enforcing the effect
await prom2();
....
}
so I have been able to enforce re-render (by enforcing the effect) while I'm still in the handleSubmit execution.
is there a simpler way? or, did I mistakenly understand the concepts of React?
update
The issue is that I have a checkout form, and I need it to be a signup form at the same time, and there is also a login component on the page.
so I need to populate the form fields with the account if information in case of login and in case of sign up.
The steps are as follow:
if user login => populate form (per fill it with user info) => move to payment.
if user fill out the form manually:
create an account.
authenticate the new user.
update the user account.
repopulate form (with data from user account).
move to payment.
so I have this function that needs to listen to the login and signup:
const token = useSelector(_token);
const loggedIn = useSelector(_loggedIn);
const profile = useSelector(_profile);
useEffect(() => {
/**
* Pre-fill the form inputs
*/
(async () => {
const r = await dispatch(fetchUserInfo());
setFormProfile(profile); // address is not updated yet
setFormAddress(r?.result?.address);
})();
}, [loggedIn, forceUpdate]);
now, there are no issues with the login process, the only problem is with the signup:
at step 2, when authenticating the user, its account is empty.
so the loggedIn changes to true when the profile is empty so I got empty form.
after updating the profile, loggedIn will not change, so I need another variable to trigger the effect again.
I tried to listen to profile here, but I got an infinite loop.
and here is the checkout flow related to the signup:
...
if (!loggedIn) {
const signupResponse = await dispatch(signupUser(params));
loginResponse = await dispatch(login(formProfile?.email, password));
}
const updateProfileResponse = await saveChangesToProfile();
// update user profile with the information in the checkout form.
...
then save changes to the profile:
const saveChangesToProfile = async () => {
const r = await dispatch(fetchUserInfo());
const addressID = r?.result?.address_id;
const res1 = await dispatch(updateUserAddress(addressID, { ID: addressID, ...formAddress }));
const res = await dispatch(UpdateUser(r?.result?.ID, formProfile));
setForceUpdate(Math.random()); // force re-render to re-populate the form.
setSuccess("Information saved to your profile!");
return res;
};
Update 2
The question is general, I solved the issue in another way days ago (involving changes to the server routes). and I'm asking the question in a general way to get some knowledge, not for others to do the work for me.
In general, you should avoid having to force an update in React but instead use existing React features to accomplish your goal. That being said, there are simple ways to force a re-render in react. You mentioned in the second update that you are looking for more general solutions - so I will provide them here.
However, please bear in mind that this topic has been discussed extensively in other stack overflow questions (I will provide links).
Forcing Re-Render using component.forceUpdate(callback)
The react docs actually list a simple way to force a component to reload (provided you maintain a reference to it). You can find more information here, but essentially it forces your component to re-render and then makes a call to the callback argument.
Forcing Re-Render using hooks
There are multiple stack overflow questions that provide simple code snipets that can force a react component to re-render by using hooks. This answer for example by #Qwerty demonstrates 2 simple code snipets to force a re-render:
const forceUpdate = React.useState()[1].bind(null, {}) // see NOTE above
const forceUpdate = React.useReducer(() => ({}))[1]
You should check out his answer for a more detailed explanation.
Other sources include this answer to the same stack overflow question that references the official FAQ.
It solves the problem by doing:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
Solving Your Specific Problem
I saw that you were able to solve your problem by using the useEffect hook - a great start for a potential solution. You also mentioned that you got an infinite loop while listening to a variable change in your hook - a common problem and one with some common solutions. In general, you should always run a check inside the useEffect hook before changing any of its dependencies. For example, run a check to see if the profile is unset before trying to update its value.
I however would recomend that you use a progress varible that would indicate your status, something like this:
const STATUS_START = 0;
const STATUS_LOGED_IN = 1;
const STATUS_SIGNING_UP = 2;
const [progress, setProgress] = useState(STATUS_START);
Then, you can simply listen to changes made to the progress variable in your useEffect hook (by passing it as your only dependent). This should automatically condition you to write the necessary logic to check for state inside of the useEffect function as I described previously.
This solution would work by initially setting the progress to either signing up or logging in, but only filling the form data if you are logged in (and after the signup progress is done calling setProgress(STATUS_LOGED_IN))

Memory leak/infinite loop issue with multiplayer game: React State and Firestore snapshot listener

I am trying to write a simple multiplayer game involving a board of tiles. I need real time updates so that all players see all tile updates when they happen. This is my first time using Firestore or building a game with real time updates. I am using React with React Hooks to build my game.
const [tiles, setTiles] = useState([]);
Tiles is an array of 25 objects. The logic for starting a new game and initial setting of new batch of tiles is elsewhere, and I don't think relevant for the problem.
On Firestore, I have a games collection. Each games document includes the tiles array.
When first loaded, we use useEffect with an empty dependency array to subscribe to the onSnapshot listener for the game, and update our local state when snapshots are received.
I used the example from the documentation to attempt to only update the local state when the updates are from the server: https://firebase.google.com/docs/firestore/query-data/listen?authuser=0#events-local-changes
useEffect(() => {
return db
.collection('games')
.doc(code)
.onSnapshot((doc) => {
var source = doc.metadata.hasPendingWrites ? 'Local' : 'Server';
if (source !== 'Local') {
console.log('Game snapshot!');
// get the tiles from the database and update local state to reflect
setTiles(doc.data().tiles);
}
})
}, []
)
When turns happen, tiles are clicked and then new state of the tiles is set after the click:
const onTileClick = (tile, index) => {
// ...
// perform some logic, calculate value of new_tiles
// ...
// Then update tiles in local state
setTiles(new_tiles);
}
And whenever the tiles change, we again use useEffect() to monitor for tile changes and update the Firestore game.
useEffect(() => {
return db
.collection('games')
.doc(code)
// send the new tiles to the server
.update({tiles: tiles})
.then((res) => {
console.log('Tiles updated!');
})
.catch((res) => {
console.log('Error updating tiles!', res);
});
}, [tiles]);
The problem, as you can probably see, is that whenever I click a tile, it starts an infinite loop of setting tiles, triggering a snapshot, setting the tiles again.. etc...
This is only happening when I have two browser windows open (one in incognito mode to mimic two players), I think what's happening is, when the tiles are set from one player's window, it triggers the snapshot for the other player, which sets their tiles, which triggers a snapshot for the other player, setting their tiles ...etc...
Clearly I am not using React Hooks in the best way here. Does anyone have any better suggestions of how to manage React state with a Firestore snapshot? Or do I just need to be more granular in the onSnapshot listener to check for actual differences before updating React state? The problem is, there are going to be multiple fields once I've got a bit further, not just 'tiles'... so more differences to check for.
Thanks in advance.
Consider implementing some way to determine if setTiles should actually be called in your snapshot callback. Right now you are checking doc.metadata.hasPendingWrites, which doesn't tell you for certain if something actually changed. It just tells you if the server sent you an update. Consider writing some logic to compare the current and prior contents of the document to see if something actually changed that needs rendering, and only call setTiles if that's the case. That might stop the ping-ponging of data between the users.

When object stored in useContext updates DOM does not rerender

I am writing an application that basically is a game. The information about the game is stored in a context provider that wraps the entire application. I have written prototype methods for the game object and call them in the react app but the changes do not appear on the page.
const onDeckEl = useRef(null)
useEffect(() => {
let onDeck = round.getOnDeck()
onDeckEl.current = onDeck
console.log(onDeckEl)
}, [round])
I can see in the dev tools that the values of the arrays in the round object are changing when I want but the console.log is never fired. Can someone explain why?
The most likely reason the console.log is not firing is because the round variable isn't changing. The code:
useEffect(() => {
console.log('Effect!');
}, [round])
wont fire unless the round variable changes.

how to emulate messages/events with react useState and useContext?

I'm creating a react app with useState and useContext for state management. So far this worked like a charm, but now I've come across a feature that needs something like an event:
Let's say there is a ContentPage which renders a lot of content pieces. The user can scroll through this and read the content.
And there's also a BookmarkPage. Clicking on a bookmark opens the ContentPage and scrolls to the corresponding piece of content.
This scrolling to content is a one-time action. Ideally, I would like to have an event listener in my ContentPage that consumes ScrollTo(item) events. But react pretty much prevents all use of events. DOM events can't be caught in the virtual dom and it's not possible to create custom synthetic events.
Also, the command "open up content piece XYZ" can come from many parts in the component tree (the example doesn't completely fit what I'm trying to implement). An event that just bubbles up the tree wouldn't solve the problem.
So I guess the react way is to somehow represent this event with the app state?
I have a workaround solution but it's hacky and has a problem (which is why I'm posting this question):
export interface MessageQueue{
messages: number[],
push:(num: number)=>void,
pop:()=>number
}
const defaultMessageQueue{
messages:[],
push: (num:number) => {throw new Error("don't use default");},
pop: () => {throw new Error("don't use default");}
}
export const MessageQueueContext = React.createContext<MessageQueue>(defaultMessageQueue);
In the component I'm providing this with:
const [messages, setmessages] = useState<number[]>([]);
//...
<MessageQueueContext.Provider value={{
messages: messages,
push:(num:number)=>{
setmessages([...messages, num]);
},
pop:()=>{
if(messages.length==0)return;
const message = messages[-1];
setmessages([...messages.slice(0, -1)]);
return message;
}
}}>
Now any component that needs to send or receive messages can use the Context.
Pushing a message works as expected. The Context changes and all components that use it re-render.
But popping a message also changes the context and also causes a re-render. This second re-render is wasted since there is no reason to do it.
Is there a clean way to implement actions/messages/events in a codebase that does state management with useState and useContext?
Since you're using routing in Ionic's router (React-Router), and you navigate between two pages, you can use the URL to pass params to the page:
Define the route to have an optional path param. Something like content-page/:section?
In the ContentPage, get the param (section) using React Router's useParams. Create a useEffect with section as the only changing dependency only. On first render (or if section changes) the scroll code would be called.
const { section } = useParams();
useEffect(() => {
// the code to jump to the section
}, [section]);
I am not sure why can't you use document.dispatchEvent(new CustomEvent()) with an associated eventListener.
Also if it's a matter of scrolling you can scrollIntoView using refs

Firebase 'child_changed' listener understanding

I am trying to grab real-time changes from Firebase on the status of a question in the lifecycle of my system. My child_changed event is interacting differently than I was expecting, so I am not sure if I am using it right.
I want to grab the new change to the questionStatus variable, so that if another user on the system changes the variable on the database, the updates will be seen real time for both users. My Firebase DB is setup like so:
-/questions
-/ID1234567890
-/title
-/messages
-/ID1234567890
-/ID2345678901
'child_changed' call:
fire.database().ref('/questions/' + this.state.questionId + '/questionStatus').on('child_changed', snap => {
this.setState({
questionStatus: (snap.val().questionStatus),
})
console.log("CHANGED!")
})
The above code is how I think it should work, but I can never get the console.log to display as intended. By just removing questionStatus from the ref() (as outlined in the code below), the event triggers whenever I add a new message to the question (for chat), and seems to be causing a loading error. Am I misunderstanding firebase or is deeper in my code? Maybe this has to with lifecycle of component? I have tried placing it everywhere to no avail, but currently have it in componentWillMount(). Let me know what you think, thanks!
fire.database().ref('/questions/' + this.state.questionId).on('child_changed', snap => {
this.setState({
questionStatus: (snap.val().questionStatus),
})
console.log("CHANGED!")
})
Based on my research, I didn't full understand setState() fully, not knowing that the state can only be updated async, not sync. Knowing this, I just implemented a feature informing the user that the status has changed and to click a button to reload. Not ideal, but it will work for now.
I also tested to exclude children with a specific key (messages).
So my new code looks like this:
fire.database().ref('/questions/' + this.state.questionId).on('child_changed', snap => {
// Ignore if messages are being added to the ticket
if(snap.key !== 'messages'){
this.setState({
questionStatus: (snap.val().questionStatus),
questionTags: (snap.val().questionTags),
question
})
}
})

Categories