I am encountering an issue in my Firebase Group Counter app. If I make any CRUD changes in my app, all items that my onSnapshot function listens to are rewritten by this function and displayed in a different order. How can I ensure that the items are always displayed consistently, with the oldest item at the top and the newest at the bottom of my items list?
hier is app live page: click
and this is GitHub repo: click
this is my ContextProvider
component with CRUD logic:
export const ContextAPI = createContext<null | TContextAPI>(null);
const ContextProvider = ({ children }: { children: React.ReactNode }) => {
const navigate = useNavigate();
const { groupId } = useParams();
const [currentUserId, setCurrentUserId] = useState<string | undefined | null>(undefined);
const [groupItemsData, setGroupItemsData] = useState<DocumentData | null>(null);
const [groupList, setGroupList] = useState<TGroupList[] | null>(null);
const [currentGroup, setCurrentGroup] = useState<string>("");
const [currentPageName, setCurrentPageName] = useState<string>("Menu");
const [newGroupName, setNewGroupName] = useState<string>("");
const [isShowGroupCreator, setIsShowGroupCreator] = useState<boolean>(false);
const addValueHandler = () => {
if (currentUserId) {
const groupRef = doc(db, "user_groups", currentUserId, "groups", newGroupName);
setDoc(groupRef, {});
}
alert("added...");
};
const handleUserGroups = () => {
if (newGroupName === "" || newGroupName.length > 20) {
console.log("group name is incorrect");
} else {
addValueHandler();
navigate("/");
}
};
const getValue = () => {
if (currentUserId) {
const userGroupRef = doc(db, "user_groups", currentUserId);
const collectionVal = query(collection(userGroupRef, "groups"));
onSnapshot(collectionVal, (doc) => {
setGroupList(doc.docs.map((doc) => ({ id: doc.id.replace(/ /g, "-"), name: doc.id })));
});
}
};
const displayGroupItemsHandler = useCallback(
(groupId: string, currentUserId: string | null | undefined) => {
const groupsRef = doc(db, `user_groups/${currentUserId}/groups/${currentGroup}`);
console.log("docId: ");
console.log("groupId: " + groupId);
console.log(groupItemsData);
onSnapshot(groupsRef, (querySnapshot) => {
if (querySnapshot.id.replace(/ /g, "-") === groupId) {
// Only update state if data has changed
if (!groupItemsData || JSON.stringify(querySnapshot.data()) !== JSON.stringify(groupItemsData)) {
setGroupItemsData({ ...querySnapshot.data() });
}
}
});
},
[currentGroup, groupItemsData]
);
const vals: TContextAPI = {
currentPageName,
setCurrentPageName,
currentGroup,
setCurrentGroup,
groupItemsData,
displayGroupItemsHandler,
addValueHandler,
groupList,
setNewGroupName,
isShowGroupCreator,
setIsShowGroupCreator,
newGroupName,
handleUserGroups,
currentUserId,
setCurrentUserId,
};
useEffect(() => {
if (currentUserId) {
getValue();
}
}, [currentUserId]);
useEffect(() => {
if (currentUserId && groupId && currentGroup) {
displayGroupItemsHandler?.(groupId, currentUserId);
}
}, [currentUserId, groupId, currentGroup, displayGroupItemsHandler]);
return <ContextAPI.Provider value={vals}> {children} </ContextAPI.Provider>;
};
export default ContextProvider;
And this is a GroupContent
component, where i displaying my list items:
const GroupContent = () => {
const { groupId } = useParams();
const navigate = useNavigate();
const groupItemsData = useContextSelector(ContextAPI, (v) => v?.groupItemsData);
const groupList = useContextSelector(ContextAPI, (v) => v?.groupList);
const isGroupContentUrlCorrect = groupList?.find((group) => group.id === groupId);
const setCurrentPageName = useContextSelector(ContextAPI, (v) => v?.setCurrentPageName);
const setCurrentGroup = useContextSelector(ContextAPI, (v) => v?.setCurrentGroup);
useEffect(() => {
const group = groupList?.find((item) => item.id === groupId);
if (group) {
setCurrentGroup?.(group.name);
}
}, [groupList, groupId, setCurrentGroup]);
return (
<>
<SBox>
<Stack
direction={"row"}
justifyContent={"space-between"}
marginBottom={3}
flexWrap={"wrap"}
alignItems="center"
gap={2}
>
<Button
sx={{ width: "170px" }}
color="secondary"
variant="contained"
onClick={() => {
navigate(`/`);
setCurrentPageName?.("Menu");
}}
>
Go back
</Button>
<Box
display="flex"
gap={2}
sx={{
"@media(max-width:530px)": {
flexDirection: "column",
alignItems: "flex-end",
},
"@media(max-width:403px)": {
flexDirection: "column",
alignItems: "baseline",
},
}}
>
<Button sx={{ width: "170px" }} variant="contained" onClick={() => navigate(`/${groupId}/new-field`)}>
Add New Field
</Button>
<Button sx={{ width: "100px" }} variant="contained" onClick={() => navigate(`/${groupId}/print-content`)}>
Print
</Button>
</Box>
</Stack>
<GroupContentProvider>
{isGroupContentUrlCorrect ? (
<>
<Grid container spacing={2} overflow={"auto"}>
{groupItemsData &&
Object.entries(groupItemsData).map(([label, value]) => (
<Grid key={label} item xs={12} id={label}>
<Card sx={{ display: "flex", width: "100%", height: 60 }}>
<CardActionArea
sx={{ display: "flex", justifyContent: "flex-start", gap: "0.8rem" }}
onClick={() => {
navigate(`/${groupId}/${label.toLowerCase().replace(/ /g, "-")}`);
}}
>
<Box
height="100%"
width={60}
sx={{ backgroundColor: "#e5fdfc", display: "grid", placeContent: "center" }}
>
<Typography fontSize={28}>✅</Typography>
</Box>
<Box
sx={{
width: "100%",
paddingInline: "1rem",
display: "flex",
justifyContent: "space-between",
}}
>
<Typography sx={{ wordBreak: "break-all" }} variant="h6" component="div">
{label}
</Typography>
<Typography sx={{ width: "3rem" }} fontWeight={600} variant="h5" component="div">
{value}
</Typography>
</Box>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
<Outlet />
</>
) : (
<h2>Page not found</h2>
)}
</GroupContentProvider>
</SBox>
</>
);
};
I’ve tried to wrap my displayGroupItemsHandler
function in useCallback
.