How to automatically update data after a change - reactjs

I am making a small application for writing words, using a Firestore Cloud for data storage. I use onSubmit to send data to Firestore for storage.
Everything works and is saved to the cloud, but the data is not updated on the page, for which you need to reload the page and after that the data will already be updated.
How can I solve this problem, do I need to render a new array every time and get data from the server in order to update the data (fetchProduct), or is it possible in some other way?
function Menu() {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const { user } = UserAuth();
const fetchProduct = async () => {
const ref = collection(db, "langcards-db");
const q = query(ref, where("author", "==", user.uid));
const querySnapshot = await getDocs(q);
const arr = [];
querySnapshot.forEach((doc) => {
arr.push({
...doc.data(),
id: doc.id,
});
});
setData(arr);
setIsLoading(false);
};
useEffect(() => {
if (user) {
fetchProduct();
} else {
setData([]);
setIsLoading(false);
}
}, []);
useEffect(() => {
if (user) {
fetchProduct();
} else {
setData([]);
setIsLoading(false);
}
}, [user]);
const onSubmit = async (e) => {
e.preventDefault();
let rw = word.replace(/\s/g, "");
let rt = translate.replace(/\s/g, "");
if (rw.length >= 1 && rt.length >= 1) {
setWarn(false);
await addDoc(collection(db, "langcards-db"), {
word: word,
translate: translate,
note: note,
category: "category",
author: user.uid,
});
setWord("");
setTranslate("");
setNote("");
console.log(data)
} else {
setWarn(true);
}
};
return (
<div className="main-inner">
{isLoading ? (
<Loader />
) : (
<div className="content">
<Routes>
<Route
path="addcard"
element={
<AddCard
onChangeWord={onChangeWord}
onChangeNote={onChangeNote}
onChangeTranslate={onChangeTranslate}
onSubmit={onSubmit}
word={word}
translate={translate}
note={note}
warn={warn}
resetFormAdd={resetFormAdd}
/>
}
/>
<Route
path="saved"
element={<Saved data={data} setData={setData} />}
/>
</Route>
</Routes>
</div>
);
}
export default Menu;

For this problem you should not use getDocs but onSnapshot which listens for changes in the data. Also, this way you can update the state directly and don`t have to create a new array each time. Then you can directly refer to .data when accessing an entry from the data state.
useEffect(() => {
const ref = collection(db, "langcards-db");
const q = query(ref, where("author", "==", user.uid));
const unsubscribe = onSnapshot(q, { includeMetadataChanges: true }, (querySnapshot) => {
setData([]); // Empty state
querySnapshot.forEach((doc) => {
// Do something with your data (append it to the data array like before)
setData(old => {...old, {"id" : doc.id, "data" : doc.data()});
});
});
return () => { unsubscribe(); };
}, [])
In this doc you can read more about how to fetch data from your firestore.

Related

How to effectively refresh the data in a custom hook using react-infinite-scroll-component when an item is deleted?

I am using a custom hook useInfiniteFetchSearch to fetch and search data for a infinite scroll component built using react-infinite-scroll-component.
The hook makes an API call and sets the data in the state using setData. Currently, I am using refreshData() method to refresh the data again when an item is deleted from the list.
However, I am not satisfied with this solution as it calls the API again even though I already have the data. Is there a more efficient way to refresh the data and update the infinite scroll component without making another API call?
Here is my custom hook implementation:
import { useState, useEffect, useRef } from "react";
import axios from "axios";
const useInfiniteFetchSearch = (api, resultsPerPage, sort = null) => {
const [data, setData] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(2);
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const searchTermRef = useRef(null);
useEffect(() => {
const searchData = async () => {
try {
setLoading(true);
let query = `${api}${
searchTerm === "" ? `?` : `?search=${searchTerm}&`
}page=1`;
query = sort ? `${query}&sort=${sort}` : query;
const result = await axios.post(query);
const fetchedData = result.data;
setData(fetchedData);
setPage(2);
setHasMore(fetchedData.length === resultsPerPage);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
searchData();
}, [searchTerm, api, resultsPerPage, sort]);
const refreshData = async () => {
try {
setLoading(true);
let query = `${api}${
searchTerm === "" ? `?` : `?search=${searchTerm}&`
}page=1`;
query = sort ? `${query}&sort=${sort}` : query;
const result = await axios.post(query);
const fetchedData = result.data;
setData(fetchedData);
setPage(2);
setHasMore(fetchedData.length === resultsPerPage);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
const fetchMore = async () => {
try {
setLoading(true);
let query = `${api}?search=${searchTerm}&page=${page}`;
query = sort ? `${query}&sort=${sort}` : query;
const result = await axios.post(query);
const newData = result.data;
setData((prev) => [...prev, ...newData]);
setPage(page + 1);
setHasMore(newData.length === resultsPerPage);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
const handleSearch = async (e) => {
e.preventDefault();
setSearchTerm(searchTermRef.current.value);
};
const handleDelete = async (e, itemId) => {
try {
await axios.delete(`${api}${itemId}`);
setData((prevData) => prevData.filter((item) => item.id !== itemId));
refreshData();
} catch (error) {
console.log(error);
} finally {
}
};
return {
state: { data, hasMore, loading, searchTermRef, searchTerm },
handlers: {
fetchMore,
setSearchTerm,
handleSearch,
handleDelete,
},
};
};
export default useInfiniteFetchSearch;
I am using this hook in my component:
const { state, handlers } = useInfiniteFetchSearch("/api/guides/search", 5);
const { data, hasMore, loading, searchTermRef, searchTerm } = state;
const { fetchMore, handleSearch, setSearchTerm, handleDelete } = handlers;
....
<InfiniteScroll
dataLength={data.length}
next={fetchMore}
hasMore={hasMore}
scrollableTarget="scrollableDiv"
loader={
<div className="flex justify-center items-center mx-auto">
<Loader />
</div>
}
>
<div className="space-y-1">
{data &&
data.map((item, index) => (
<GuidesItem
key={index}
guide={item}
handleDelete={handleDelete}
/>
))}
</div>
</InfiniteScroll>
I would appreciate any suggestions or solutions to this problem, thank you!

React useEffect with async/await issue

I am trying to add some properties to an array of objects inside useEffect but it renders DOM while those fields are still not present. Is there any reliable way how to wait till those properties are added:
useEffect hook look like this:
useEffect(() => {
onSnapshot(query(collection(db, "conversations"), where('canRead', 'array-contains', user.user.uid), orderBy("lastMsgDate", 'desc')),
async (snapshot) => {
let conversations = snapshot.docs.map(doc => toConversation(doc, user.user.uid));
await conversations.map(async (convo, index) => {
const profile = await getDoc(doc(db, "users", convo.canRead[0]))
conversations[index].picture = profile.data().picture
conversations[index].name = profile.data().name
})
setConversations(conversations)
})
}, []);
This is how I am rendering list of convos:
<IonCard>
{conversations.length > 0 ?
conversations.map((conversation) =>
<IonItem key={conversation.id}>
<IonAvatar slot='start'>
<img src={conversation.picture ? conversation.picture : '/assets/default-profile.svg'} alt={conversation.name} />
</IonAvatar>
<IonLabel>{conversation.name}</IonLabel>
</IonItem>
)
:
<IonCardContent>
no convos
</IonCardContent>
}
</IonCard>
the name and picture does not render even i can see it when log array into console
0:{
canRead: ['iwPmOBesFQV1opgs3HT9rYPF7Sj1'],
id: "W6cefXGoBAZijPof8jVl",
lastMsg: "test",
lastMsgDate: nt {seconds: 1668418292, nanoseconds: 281000000},
lastMsgSender: "Hyw4Argt8rR25mFaFo1Sl4iAWoM2",
name: "Warren"
picture: "https://firebasestorage.googleapis.com/v0/b/..."
users: {
Hyw4Argt8rR25mFaFo1Sl4iAWoM2: true,
iwPmOBesFQV1opgs3HT9rYPF7Sj1: true
}
}
Any help appreciated
You can use a loading message or a gif.
const [loading, setLoading] = useState(true);
useEffect(() => {
onSnapshot(query(collection(db, "conversations"), where('canRead', 'array-contains', user.user.uid), orderBy("lastMsgDate", 'desc')),
async (snapshot) => {
....
setConversations(conversations);
setLoading(false);
})
}, []);
if(loading) return <p>Loading...</p>
return <> your content </>
for some reason it works with setTimeout function which is not the best solution
useEffect(() => {
setLoading({ loading: true, loadingMsg: 'Loading conversations' })
onSnapshot(query(collection(db, "conversations"), where('canRead', 'array-contains', user.user.uid), orderBy("lastMsgDate", 'desc')),
async (snapshot) => {
let convos = snapshot.docs.map(doc => toConversation(doc, user.user.uid));
await convos.map(async (convo, index) => {
const profile = await getDoc(doc(db, "users", convo.canRead[0]))
convos[index].picture = profile.data().picture
convos[index].name = profile.data().name
})
setTimeout(() => {
setConversations(convos)
setLoading({ loading: false, loadingMsg: undefined })
}, 1000);
})
}, [user.user.uid]);
Maybe u can add a loading state? something like
const [loading, setLoading] = useState(false);
const [conversations,setConversations] = useState(null);
const onSnapshot = async () => {
setLoading(true)
onSnapshot(
query(
collection(db, "conversations"),
where("canRead", "array-contains", user.user.uid),
orderBy("lastMsgDate", "desc")
),
async (snapshot) => {
let conversations = snapshot.docs.map((doc) =>
toConversation(doc, user.user.uid)
);
await conversations.map(async (convo, index) => {
const profile = await getDoc(doc(db, "users", convo.canRead[0]));
conversations[index].picture = profile.data().picture;
conversations[index].name = profile.data().name;
});
setConversations(conversations);
}
);
setLoading(false);
};
useEffect(() => {
onSnapshot()
},[])
return loading ? <span>loading...</span> : <div>{conversations.map((con,i) => <span key={i}>con</span>)}</div>

React not updating state?

I´m new to react. I´m trying to fetch an endpoints array. and I want to update the api's status every 15 seconds. I´m trying to do this
export const endpoints: string[] = [
"accounts/health/status",
"assets/health/status",
"customers/health/status",
"datapoints/health/status",
"devices/health/status",
"documents/health/status",
"forms/health/status",
"invites/health/status",
"media/health/status",
"messages/health/status",
"namespaces/health/status",
"orders/health/status",
"patients/health/status",
"relationships/health/status",
"rules/health/status",
"templates/health/status",
"users/health/status",
"workflows/health/status",
];
and I have this proxy in my package.json
"proxy": "https://api.factoryfour.com/",
Here the rest of my code
const [data, setData] = useState<Response[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string[] | null[]>([]);
const effectRan = useRef(false);
const fetching = async () => {
setLoading(true);
endpoints.map(async (endpoint) => {
return await axios
.get(endpoint)
.then((res) => {
setData((prev) => [...prev, res.data]);
})
.catch((err) => {
setError([...error, err.message]);
});
});
setLoading(false);
};
useEffect(() => {
if (!effectRan.current) {
fetching();
}
return () => {
effectRan.current = true;
};
});
useEffect(() => {
setTimeout(async () => {
setData([]);
setLoading(true);
setError([]);
await fetching();
}, 15000);
}, []);
but when the seTimeout runs every card duplicates and the state gets more data than before. even though I´m reseting the state to setData([]) I just want to update the api's status. What can i do?
if (loading) return <Spinner />;
return (
<div className="card-container">
{data.length ? (
data.map((item) => {
return (
<Card
key={generateKey()}
hostname={item.hostname}
message={item.message}
success={item.success}
time={item.time}
/>
);
})
) : (
<Spinner />
)}
{error.length
? error.map((err) => (
<ErrorCard key={generateKey()} message={err as string} />
))
: null}
</div>
```
Theres a few things wrong here and one or more probably fixes it:
You keep a ref around to track the first fetch but theres no need as you can do that by virtue of using [] in an effects deps array, which you already have.
The loading state does not wait until all requests in flight finished.
The 15 second interval does not wait until all requests launched are finished.
You dont clear down the timer if the component unmounts and remounts.
The data is not keyed against the endpoint which could land you in trouble if using React strictmode that runs affects twice in dev mode.
Your code, by design it seems, does append data each time one of the requests comes back -- but I think that was intentional?
const [data, setData] = useState<Record<string, Response>>({});
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Record<string, string | null>>({});
const fetching = async () => {
setLoading(true);
await Promise.all(
endpoints.map((endpoint) => {
return axios
.get(endpoint)
.then((res) => {
setData((prev) => ({...prev, [endpoint]: res.data}));
})
.catch((err) => {
setError((prev) => ({...prev, [endpoint]: err.message}));
});
})
);
setLoading(false);
};
useEffect(() => {
let timer: number | null = null;
const intervalFetch = async () => {
await fetching();
timer = setTimeout(async () => {
setError({});
setData({});
intervalFetch();
}, 15000);
};
intervalFetch();
return () => timer !== null && clearTimeout(timer);
}, []);
if (loading) return <Spinner />;
return (
<div className="card-container">
{Object.values(data).length ? (
Object.values(data).map((item) => {
return (
<Card
key={generateKey()}
hostname={item.hostname}
message={item.message}
success={item.success}
time={item.time}
/>
);
})
) : (
<Spinner />
)}
{Object.values(error).length
? Object.values(error).map((err) => (
<ErrorCard key={generateKey()} message={err as string} />
))
: null}
</div>)
I think this piece of code might be adding additional data instead of overwriting the existing one. Is that what you're trying to do?
setData((prev) => [...prev, res.data]);

Render Cards after fetch data from api and create an object

I'm trying to learn ReactJS..
Today I was trying to create an array of objects with fetch results and after that create the cards, but I just can update the state but the cards are not re-render.. can you help me?
App.js
const teamsForLoop = [
Team1,
Team2,
Team3
];
const [allPlayers, setAllPlayers] = useState([]);
const [team, setTeam] = useState([]);
const [allTeams] = useState(teamsForLoop);
const [loading, setLoading] = useState(true);
useEffect(() => {
const playerInfo = async() => {
setLoading(true)
allTeams.map(async(teamArray) => {
setTeam([])
teamArray.map(async (player) => {
let playerName = player.split(" ");
const result = await axios.get(
`https://www.thesportsdb.com/api/v1/json/2/searchplayers.php?p=${playerName[0]}%20${playerName[1]}`
);
if (result.data.player === null) {
setTeam((state) => {
return [...state];
});
} else {
setTeam((state) => {
return [...state, result.data.player[0]];
});
}
});
setAllPlayers(team);
});
setLoading(false);
};
playerInfo();
},[]);
if (loading) return "...Loading...";
return (
<>
<PlayerList allPlayers={allPlayers} />
</>
);
}
export default App;
PlayerList.js
function PlayerList({ allPlayers }) {
const myData = []
.concat(allPlayers)
.sort((a, b) => (a.idTeam !== b.idTeam ? 1 : -1))
return (
<div>
{myData.map((player,index) =>
(
<div key={index}>
...........
</div>
)
)}
</div>
);
}
I think my problem was on the useEffect hook or maybe on my fetch function..
I already have done it using just arrays but without state.
Issue
The issue I see now is that you are attempting to cache the fetched players in the team state in the loops and then use the team state to update the players state. The problem here is that React state updates are asynchronously processed, so team hasn't updated when setAllPlayers(team); is called.
Solution
It would be simpler to map the allTeams arrays to the GET requests, wait for them to resolve, and enqueue a single allPlayers state update. Flatten the arrays of team's players and map these to the axios GET Promise. Wait for these to resolve and map the results to the array of players.
Example:
function App() {
const [allPlayers, setAllPlayers] = useState([]);
const [allTeams] = useState(teamsForLoop);
const [loading, setLoading] = useState(true);
const playerInfo = async () => {
setLoading(true);
const response = await Promise.all(
allTeams
.flat()
.map((player) =>
axios.get(
`https://www.thesportsdb.com/api/v1/json/2/searchplayers.php?p=${player}`
)
)
);
const players = response.map((result) => result.data.player[0]);
setAllPlayers(players);
setLoading(false);
};
useEffect(() => {
playerInfo();
}, []);
if (loading) return "...Loading...";
return <PlayerList allPlayers={allPlayers} />;
}

useState(new Map()) is not working, but object does

I honestly have no idea what is going on here. I have this code, on first render it should fetch popular repos and set them to the repos state, which should cause a re-render and paint the new repos on the DOM. The reason I use Map/obj is because I'm caching the repos to avoid re-fetch.
The code doesn't work as expected, it's not setting any new state, and I can verify it in the react dev tools. For some reason if I click around on Components in the devtools, the state updates(?!), but the DOM is still not painted (stuck on Loading), which is a very strange behavior.
export default () => {
const [selectedLanguage, setSelectedLanguage] = useState('All');
const [error, setError] = useState(null);
const [repos, setRepos] = useState(() => new Map());
useEffect(() => {
if (repos.has(selectedLanguage)) return;
(async () => {
try {
const data = await fetchPopularRepos(selectedLanguage);
setRepos(repos.set(selectedLanguage, data));
} catch (err) {
console.warn('Error fetching... ', err);
setError(err.message);
}
})();
}, [selectedLanguage, repos]);
const updateLanguage = useCallback(lang => setSelectedLanguage(lang), []);
const isLoading = () => !repos.has(selectedLanguage) && !error;
return (
<>
<LanguagesNav
selected={selectedLanguage}
updateLanguage={updateLanguage}
/>
{isLoading() && <Loading text="Fetching repos" />}
{error && <p className="center-text error">{error}</p>}
{repos.has(selectedLanguage)
&& <ReposGrid repos={repos.get(selectedLanguage)} />}
</>
);
};
However, if I change up the code to use object instead of a Map, it works as expected. What am I missing here? For example, this works (using obj instead of a Map)
const Popular = () => {
const [selectedLanguage, setSelectedLanguage] = useState('All');
const [error, setError] = useState(null);
const [repos, setRepos] = useState({});
useEffect(() => {
if (repos[selectedLanguage]) return;
(async () => {
try {
const data = await fetchPopularRepos(selectedLanguage);
setRepos(prev => ({ ...prev, [selectedLanguage]: data }));
} catch (err) {
console.warn('Error fetching... ', err);
setError(err.message);
}
})();
}, [selectedLanguage, repos]);
const updateLanguage = useCallback(lang => setSelectedLanguage(lang), []);
const isLoading = () => !repos[selectedLanguage] && !error;
return (
<>
<LanguagesNav
selected={selectedLanguage}
updateLanguage={updateLanguage}
/>
{isLoading() && <Loading text="Fetching repos" />}
{error && <p className="center-text error">{error}</p>}
{repos[selectedLanguage]
&& <ReposGrid repos={repos[selectedLanguage]} />}
</>
);
};
repos.set() mutates the current instance and returns it. Since setRepos() sees the same reference, it doesn't trigger a re-render.
Instead of
setRepos(repos.set(selectedLanguage, data));
you can use:
setRepos(prev => new Map([...prev, [selectedLanguage, data]]));

Resources