Change not able to update on commentsRef.on("child_changed", (data) => { - reactjs

Hi I am using firebase with react, the issue when I am trying to use
commentsRef.limitToLast(1).on("child_changed", (data) => {
if (guests) {
const key = data.key;
const updatedGuest = data.val();
//guests ->isnt updated from last trigger
const cloneGuests =_.cloneDeep(guests);
cloneGuests.forEach((element, index) => {
if (element.guestID === key) {
cloneGuests[index] = updatedGuest;
}
});
setGuests(cloneGuests);
}
});
and I am trying to use guests with my prev state and the new data, the issue that prev state isnt updated because the trigger of firebase is faster then change state,

you can debounce the react paint and que the updates
let changes = [];
const storeData = (changedData) => {
changes.push({
key: changedData.key,
updatedGuest: changedData.val()
});
}
const updateDisplay = () => {
for (canst guest in storeData){
const cloneGuests =_.cloneDeep(guests);
cloneGuests.forEach((element, index) => {
if (element.guestID === key) {
cloneGuests[index] = updatedGuest;
}
});
}
setGuests(cloneGuests);
changes = [];
}
commentsRef.limitToLast(1).on("child_changed", (data) => {
if (guests) {
storeData(guests);
debounce(updateDisplay,500);
}
});

Related

useEffect function inside context unaware of state changes inside itself

I am building a messaging feature using socket.io and react context;
I created a context to hold the conversations that are initially loaded from the server as the user passes authentication.
export const ConversationsContext = createContext();
export const ConversationsContextProvider = ({ children }) => {
const { user } = useUser();
const [conversations, setConversations] = useState([]);
const { socket } = useContext(MessagesSocketContext);
useEffect(() => {
console.log(conversations);
}, [conversations]);
useEffect(() => {
if (!socket) return;
socket.on("userConversations", (uc) => {
let ucc = uc.map((c) => ({
...c,
participant: c.participants.filter((p) => p._id != user._id)[0],
}));
setConversations([...ucc]);
});
socket.on("receive-message", (message) => {
console.log([...conversations]);
console.log(message);
setConversations((convs) => {
let convIndex = convs.findIndex(
(c) => c._id === message.conversation._id
);
let conv = convs[convIndex];
convs.splice(convIndex, 1);
conv.messages.unshift(message);
return [conv, ...convs];
});
});
}, [socket]);
return (
<ConversationsContext.Provider
value={{
conversations,
setConversations,
}}
>
{children}
</ConversationsContext.Provider>
);
};
The conversations state is updated with the values that come from the server, and I have confirmed that on the first render, the values are indeed there.
Whenever i am geting a message, when the socket.on("receive-message", ...) function is called, the conversations state always return as []. When checking devTools if that is the case I see the values present, meaning the the socket.on is not updated with the conversations state.
I would appreciate any advice on this as I`m dealing with this for the past 3 days.
Thanks.
You can take "receive-message" function outside of the useEffect hook and use thr reference as so:
const onReceiveMessageRef = useRef();
onReceiveMessageRef.current = (message) => {
console.log([...conversations]);
console.log(message);
setConversations((convs) => {
let convIndex = convs.findIndex(
(c) => c._id === message.conversation._id
);
let conv = convs[convIndex];
convs.splice(convIndex, 1);
conv.messages.unshift(message);
return [conv, ...convs];
});
};
useEffect(() => {
if (!socket) return;
socket.on("userConversations", (uc) => {
let ucc = uc.map((c) => ({
...c,
participant: c.participants.filter((p) => p._id != user._id)[0],
}));
setConversations([...ucc]);
});
socket.on("receive-message", (...r) => onReceiveMessageRef.current(...r));
}, [socket]);
let me know if this solves your problem

React-query cache doesn't persist on page refresh

I would like to set a 24 hours cache once a useQuery request has succeeded.
But as soon as I refresh the page, the cache is gone. I see it because I console.log a message each time the route is hit on my server.
How to prevent this behaviour and implement a real cache?
Here is the code:
import { useQuery } from "react-query";
import { api } from "./config";
const _getUser = async () => {
try {
const res = api.get("/get-user");
return res;
} catch (err) {
return err;
}
};
export const getUser = () => {
const { data } = useQuery("contact", () => _getUser(), {
cacheTime: 1000 * 60 * 60 * 24,
});
return { user: data && data.data };
};
// then in the component:
const { user } = getUser();
return (
<div >
hello {user?.name}
</div>
I've also tried to replace cacheTime by staleTime.
if you reload the browser, the cache is gone because the cache lives in-memory. If you want a persistent cache, you can try out the (experimental) persistQueryClient plugin: https://react-query.tanstack.com/plugins/persistQueryClient
React query has now an experimental feature for persisting stuff on localStorage.
Nonetheless, I preferred using a custom hook, to make useQuery more robust and to persist stuff in localSTorage.
Here is my custom hook:
import { isSameDay } from "date-fns";
import { useEffect, useRef } from "react";
import { useBeforeunload } from "react-beforeunload";
import { useQuery, useQueryClient } from "react-query";
import { store as reduxStore } from "../../redux/store/store";
const LOCAL_STORAGE_CACHE_EXPIRY_TIME = 1000 * 60 * 60 * 23; // 23h
const divider = "---$---";
const defaultOptions = {
persist: true, // set to false not to cache stuff in localStorage
useLocation: true, // this will add the current location pathname to the component, to make the query keys more specific. disable if the same component is used on different pages and needs the same data
persistFor: LOCAL_STORAGE_CACHE_EXPIRY_TIME,
invalidateAfterMidnight: false, // probably you want this to be true for charts where the dates are visible. will overwrite persistFor, setting expiry time to today midnight
defaultTo: {},
};
const getLocalStorageCache = (dataId, invalidateAfterMidnight) => {
const data = localStorage.getItem(dataId);
if (!data) {
return;
}
try {
const parsedData = JSON.parse(data);
const today = new Date();
const expiryDate = new Date(Number(parsedData.expiryTime));
const expired =
today.getTime() - LOCAL_STORAGE_CACHE_EXPIRY_TIME >= expiryDate.getTime() ||
(invalidateAfterMidnight && !isSameDay(today, expiryDate));
if (expired || !parsedData?.data) {
// don't bother removing the item from localStorage, since it will be saved again with the new expiry time and date when the component is unmounted or the user leaves the page
return;
}
return parsedData.data;
} catch (e) {
console.log(`unable to parse local storage cache for ${dataId}`);
return undefined;
}
};
const saveToLocalStorage = (data, dataId) => {
try {
const wrapper = JSON.stringify({
expiryTime: new Date().getTime() + LOCAL_STORAGE_CACHE_EXPIRY_TIME,
data,
});
localStorage.setItem(dataId, wrapper);
} catch (e) {
console.log(
`Unable to save data in localStorage for ${dataId}. Most probably there is a function in the payload, and JSON.stringify failed`,
data,
e
);
}
};
const clearOtherCustomersData = globalCustomerId => {
// if we have data for other customers, delete it
Object.keys(localStorage).forEach(key => {
if (!key.includes(`preferences${divider}`)) {
const customerIdFromCacheKey = key.split(divider)[1];
if (customerIdFromCacheKey && customerIdFromCacheKey !== String(globalCustomerId)) {
localStorage.removeItem(key);
}
}
});
};
const customUseQuery = (queryKeys, getData, queryOptions) => {
const options = { ...defaultOptions, ...queryOptions };
const store = reduxStore.getState();
const globalCustomerId = options.withRealCustomerId
? store.userDetails?.userDetails?.customerId
: store.globalCustomerId.id;
const queryClient = useQueryClient();
const queryKey = Array.isArray(queryKeys)
? [...queryKeys, globalCustomerId]
: [queryKeys, globalCustomerId];
if (options.useLocation) {
if (typeof queryKey[0] === "string") {
queryKey[0] = `${queryKey[0]}--path--${window.location.pathname}`;
} else {
try {
queryKey[0] = `${JSON.stringify(queryKey[0])}${window.location.pathname}`;
} catch (e) {
console.error(
"Unable to make query. Make sure you provide a string or array with first item string to useQuery",
e,
);
}
}
}
const queryId = `${queryKey.slice(0, queryKey.length - 1).join()}${divider}${globalCustomerId}`;
const placeholderData = useRef(
options.persist
? getLocalStorageCache(queryId, options.invalidateAfterMidnight) ||
options.placeholderData
: options.placeholderData,
);
const useCallback = useRef(false);
const afterInvalidationCallback = useRef(null);
const showRefetch = useRef(false);
const onSuccess = freshData => {
placeholderData.current = undefined;
showRefetch.current = false;
if (options.onSuccess) {
options.onSuccess(freshData);
}
if (useCallback.current && afterInvalidationCallback.current) {
afterInvalidationCallback.current(freshData);
useCallback.current = false;
afterInvalidationCallback.current = null;
}
if (options.persist) {
if(globalCustomerId){
saveToLocalStorage(freshData, queryId);
}
}
};
const data = useQuery(queryKey, getData, {
...options,
placeholderData: placeholderData.current,
onSuccess,
});
const save = () => {
if (options.persist && data?.data) {
saveToLocalStorage(data.data, queryId);
}
};
// if there are other items in localStorage with the same name and a different customerId, delete them
// to keep the localStorage clear
useBeforeunload(() => clearOtherCustomersData(globalCustomerId));
useEffect(() => {
return save;
}, []);
const invalidateQuery = callBack => {
if (callBack && typeof callBack === "function") {
useCallback.current = true;
afterInvalidationCallback.current = callBack;
} else if (callBack) {
console.error(
"Non function provided to invalidateQuery. Make sure you provide a function or a falsy value, such as undefined, null, false or 0",
);
}
showRefetch.current = true;
queryClient.invalidateQueries(queryKey);
};
const updateQuery = callBackOrNewValue => {
queryClient.setQueryData(queryKey, prev => {
const updatedData =
typeof callBackOrNewValue === "function"
? callBackOrNewValue(prev)
: callBackOrNewValue;
return updatedData;
});
};
return {
...data,
queryKey,
invalidateQuery,
data: data.data || options.defaultTo,
updateQuery,
isFetchingAfterCacheDataWasReturned:
data.isFetching &&
!placeholderData.current &&
!data.isLoading &&
showRefetch.current === true,
};
};
export default customUseQuery;
Some things are specific to my project, like the customerId.
I'm using onBeforeUnload to delete data not belonging to the current customer, but this project specific.
You don't need to copy paste all this, but I believe it's very handy to have a custom hook around useQuery, so you can increase its potential and do things like running a callback with fresh data after the previous data has been invalidated or returning the invalidateQuery/updateQuery functions, so you don't need to use useQueryClient when you want to invalidate/update a query.

Why is UI not updating after button click in React?

I'm working on something similar like Youtube like/dislike functionality. First getting result by it's id. Then checking and filtering and lastly doing put request to the database. But the problem is that 'likes' values in the UI changes only after page refresh. I tried using useState hook and manipulate the state when getting response from put request, without succeeding. Everything happens on button click.
Any advice is strongly appreciated.
My States
const [likes, setLikes] = useState([])
const [dislikes, setDislikes] = useState([])
Getting the review
const getDislikedReviewById = (id) => {
axios.get(`https://landlordstrapi.herokuapp.com/cool-project/${id}`)
.then((response) => {
handleDislike(response.data)
})
.catch(err => {
console.log(err);
});
}
Working with functionality
const handleDislike = (data) => {
const id = data.id
setDislikes([...data.comment_info.dislikes])
setLikes([...data.comment_info.likes])
const userToken = localStorage.getItem('user') || []
const checkIfLikeExists = likes.find(item => item === userToken)
const checkIfDislikeExists = dislikes.find(item => item === userToken)
if(checkIfLikeExists && checkIfLikeExists.length) {
setLikes(likes.filter(a => a !== userToken))
setDislikes(dislikes.push(userToken))
}
if(!checkIfDislikeExists && !checkIfLikeExists) {
setDislikes(dislikes.push(userToken))
}
JSON.stringify(dislikes)
updateDislikes(data, id, dislikes, likes)
}
When everything is done sending UPDATE request
const updateDislikes = (data, id, dislikes, likes) => {
axios.put(`https://landlordstrapi.herokuapp.com/cool-project/${id}`, {
comment_info: {
likes: likes,
dislikes: dislikes,
comment: data.comment_info.comment
}
})
.then(function(response){
console.log('saved successfully')
});
}
My like button
<Votes>
<GrLike style={{cursor: "pointer"}} onClick={() => getReviewById(id)} size={10}/>
<VoteValue>
{likes && likes.length ? likes.length : null}
</VoteValue>
</Votes>
Not sure if you want to update the state or set the data returned by the api. I assumed now you would like to set the data from the api. If you want to set the state you can do so by using the old state, so pass a function to the setState call
setDislikes((oldDislikes) => [...oldDislikes, userToken]);
and I would update the handleDislike function as so
const handleDislike = (data) => {
const {
id,
comment_info: { dislikes, likes }
} = data;
let newLikes = likes;
let newDislikes = dislikes;
const userToken = localStorage.getItem("user") || [];
const likeExists = likes.find((item) => item === userToken);
const dislikeExists = dislikes.find((item) => item === userToken);
if (likeExists) {
newLikes = newLikes.filter((a) => a !== userToken);
}
if ((!dislikeExists && !likeExists) || likeExists) {
newDislikes = [...dislikes, userToken];
}
// set new values
setDislikes(newDislikes);
setLikes(newLikes);
updateDislikes(data, id, newDislikes, newLikes);
};

Can't display elements of array React

I can see my array in state, but I don't know why elements of array doesn't display on the app interface.
const [members, setMembers] = useState([])
useEffect( () => {
getMembers();
}, [props.event])
const getMembers = () => {
let new_members = [];
console.log(props.event)
props.event && props.event.uczestnicy.map(member => {
member.get().then(doc => {
let new_member;
new_member = {
...doc.data(),
id: doc.id
}
new_members.push(new_member)
})
setMembers(new_members)
})
console.log(new_members)
console.log(members)
}
[...]
{members && members.map(member => {
console.log('mem',member)
return(
<div key={member.id}>
{member.nick}
</div>
)
})}
So I can see this array in Components using React Developer Tools, but even console.log doesn't see it in the moment of performing.
And console.log(new_members) and console.log(members) result :
Your member values are fetch asynchronously, so its ideal if you set state only after all the values are resolved. For this you can use a Promise.all
const getMembers = async () => {
let new_members = [];
console.log(props.event)
if(props.event) {
const val = await Promise.all(props.event.uczestnicy.map(member => {
return member.get().then(doc => {
let new_member;
new_member = {
...doc.data(),
id: doc.id
}
return new_member
})
});
setMembers(values);
console.log(values);
}
}

React update or add to an array of objects in useState when a new object is received

Occasionally a newItem is received from a WebSocket and gets saved to useState with saveNewItem
this then kicks off the useEffect block as expected.
Update. If there is an object in the closeArray with the same openTime as the newItem I want to replace that object in closeArray with the newItem because it will have a new close
Add. If there isn't an object in the closeArray with the same open time as newItem I want to push the new item into the array.
Remove. And finally, if the array gets longer than 39 objects I want to remove of the first item.
If I add closeArray to the array of useEffect dependencies I'm going to create a nasty loop, if I don't add it closeArray isn't going to get updated.
I want usEffect to only fire off when newItem changes and not if closeArray changes, but I still want to get and set data to closeArray in useEffect
interface CloseInterface {
openTime: number;
closeTime: number;
close: number;
}
function App() {
const [newItem, saveNewItem] = useState<CloseInterface>();
const [closeArray, saveCloseArray] = useState<CloseInterface[]>([]);
useEffect(() => {
if (newItem) {
let found = false;
let arr = [];
for (let i = 0; i < closeArray.length; i++) {
if (closeArray[i].openTime === newItem.openTime) {
found = true;
arr.push(newItem);
} else {
arr.push(closeArray[i]);
}
}
if (found === false) {
arr.push(newItem)
}
if (arr.length === 39) arr.shift();
saveCloseArray(arr);
}
}, [newItem]); // <--- I need to add closeArray but it will make a yucky loop
If I do add closeArray to the useEffect dependancy array I get the error...
index.js:1 Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
in App (at src/index.tsx:9)
in StrictMode (at src/index.tsx:8)
if I don't add closeArray to the useEffect dependancy array I get this error...
React Hook useEffect has a missing dependency: 'closeArray'. Either include it or remove the dependency array react-hooks/exhaustive-deps
the second useEffect block gets the initial data for closeArray and listens to a WebSocket that updates newItem as it arrives.
useEffect(() => {
const getDetails = async () => {
const params = new window.URLSearchParams({
symbol: symbol.toUpperCase(),
interval
});
const url = `https://api.binance.com/api/v3/klines?${params}&limit=39`;
const response = await fetch(url, { method: "GET" });
const data = await response.json();
if (data) {
const arrayLength = data.length;
let newcloseArray = [];
for (var i = 0; i < arrayLength; i++) {
const openTime = data[i][0];
const closeTime = data[i][6];
const close = data[i][4];
newcloseArray.push({ openTime, closeTime, close });
}
saveCloseArray(newcloseArray);
const ws = new WebSocket("wss://stream.binance.com:9443/ws");
ws.onopen = () =>
ws.send(
JSON.stringify({
method: "SUBSCRIBE",
params: [`${symbol}#kline_${interval}`],
id: 1
})
);
ws.onmessage = e => {
const data = JSON.parse(e.data);
const value = data.k;
if (value) {
const openTime = value.t;
const closeTime = value.T;
const close = value.c;
saveNewItem({ openTime, closeTime, close });
}
};
}
};
getDetails();
}, [symbol, interval]);
In order to better write your code, you can make use of state updater callback method, so that even if you don't pass closeArray to the useEffect and it will sstill have updated values on each run of useEffect
function App() {
const [newItem, saveNewItem] = useState<CloseInterface>();
const [closeArray, saveCloseArray] = useState<CloseInterface[]>([]);
useEffect(() => {
if (newItem) {
let found = false;
saveCloseArray(prevCloseArray => {
let arr = [];
for (let i = 0; i < prevCloseArray.length; i++) {
if (prevCloseArray[i].openTime === newItem.openTime) {
found = true;
arr.push(newItem);
} else {
arr.push(prevCloseArray[i]);
}
}
if (found === false) {
arr.push(newItem)
}
if (arr.length === 39) arr.shift();
return arr;
})
}
}, [newItem]);
You want to use a useCallback to save your new array with the updated item, like so:
const [closeArray, saveCloseArray] = useState<CloseInterface[]>([]);
const updateEntry = useCallback(newItem => {
saveCloseArray(oldCloseArray => oldCloseArray.reduce((acc, item) => {
acc.push(item.openTime === newItem.openTime ? newItem : item);
return acc;
}, []));
}, []);
You'd then apply the callback function to your button or div or whatever component is being generated, EG
return (
[1, 2, 3, 4, 5].map(item => <button key={`${item}`} onClick={() => updateEntry(item)}>Click me</button>)
);
If the only reason you have newItem is to update closeArray I would consider moving that functionality into the useEffect that utilizes WebSocket. You can still use newItem if you need to do something in addition to just updating closeArray, like showing an alert or a popup, for instance. Here's what I mean:
interface CloseInterface {
openTime: number;
closeTime: number;
close: number;
}
function App() {
const [newItem, saveNewItem] = useState<CloseInterface>();
const [closeArray, saveCloseArray] = useState<CloseInterface[]>([]);
useEffect(() => {
// Do something when newItem changes, e.g. show alert
if (newItem) {
}
}, [newItem]);
useEffect(() => {
// Work with the new item
const precessNewItem = (item = {}) => {
let found = false;
let arr = [];
for (let i = 0; i < closeArray.length; i++) {
if (closeArray[i].openTime === item.openTime) {
found = true;
arr.push(item);
} else {
arr.push(closeArray[i]);
}
}
if (found === false) {
arr.push(item)
}
if (arr.length === 39) arr.shift();
saveCloseArray(arr);
// save new item
saveNewItem(item);
};
const getDetails = async () => {
const params = new window.URLSearchParams({
symbol: symbol.toUpperCase(),
interval
});
const url = `https://api.binance.com/api/v3/klines?${params}&limit=39`;
const response = await fetch(url, { method: "GET" });
const data = await response.json();
if (data) {
const arrayLength = data.length;
let newcloseArray = [];
for (var i = 0; i < arrayLength; i++) {
const openTime = data[i][0];
const closeTime = data[i][6];
const close = data[i][4];
newcloseArray.push({ openTime, closeTime, close });
}
saveCloseArray(newcloseArray);
const ws = new WebSocket("wss://stream.binance.com:9443/ws");
ws.onopen = () =>
ws.send(
JSON.stringify({
method: "SUBSCRIBE",
params: [`${symbol}#kline_${interval}`],
id: 1
})
);
ws.onmessage = e => {
const data = JSON.parse(e.data);
const value = data.k;
if (value) {
const openTime = value.t;
const closeTime = value.T;
const close = value.c;
// process new item
processNewItem({ openTime, closeTime, close });
}
};
}
};
getDetails();
}, [symbol, interval, closeArray]); // add closeArray here
}

Resources