Firestore pagination with react-redux - javascript

I try to add pagination using firebase and react redux-toolkit. I get the logic but having trouble using it with redux.
At first I wanted to set lastDoc in redux state but I got error since it is an object.
Then I changed the way and started to keep id of last document in the state. But then I can't get the firebase doc itself
const lastDocRef = firestoreDB.doc(`catalog/${publishedBooks.lastDocId}`)
is not same with
const lastDoc = snap.docs[snap.docs.length-1];
I appreciate any help how to solve this.
import { createSlice, createAsyncThunk, createEntityAdapter } from '#reduxjs/toolkit';
import firebaseService from 'app/services/firebaseService';
const firestoreDB = firebaseService.firestoreDB;
export const getPublishedBooks = createAsyncThunk('adminApp/publishedBooks/getPublishedBooks',
async (params, { dispatch, getState }) => {
const promise = firestoreDB
.collection('catalog')
.orderBy('lastPublish', 'desc')
.limit(10)
.get()
.then(snap => {
const lastDoc = snap.docs[snap.docs.length-1];
dispatch(setLastDocId(lastDoc.id));
let books = [];
snap.forEach(bookDoc => {
const id = bookDoc.id;
const data = bookDoc.data();
const lastPublish = data.lastPublish.toDate().toISOString();
books.push({ ...data, id, lastPublish });
});
return books;
})
.catch(error => {
return {}
});
const result = await promise;
return result;
}
);
export const getPublishedBooksNext = createAsyncThunk('adminApp/publishedBooks/getPublishedBooksNext',
async (params, { dispatch, getState }) => {
const { publishedBooks } = getState().adminApp;
const lastDocRef = firestoreDB.doc(`catalog/${publishedBooks.lastDocId}`)
const promise = firestoreDB
.collection('catalog')
.orderBy('lastPublish', 'desc')
.startAfter(lastDocRef)
.limit(10)
.get()
.then(snap => {
const lastDoc = snap.docs[snap.docs.length-1];
dispatch(setLastDocId(lastDoc.id));
let books = [];
snap.forEach(bookDoc => {
const id = bookDoc.id;
const data = bookDoc.data();
const lastPublish = data.lastPublish.toDate().toISOString();
books.push({ ...data, id, lastPublish });
});
return books;
})
.catch(error => {
return {}
});
const result = await promise;
return result;
}
);
const publishedBooksAdapter = createEntityAdapter({});
const initialState = publishedBooksAdapter.getInitialState({
lastDocId: null
});
export const {
selectAll: selectPublishedBooks,
selectById: selectPublishedBookById,
selectTotal: selectPublishedBooksTotal
} = publishedBooksAdapter.getSelectors(state => state.adminApp.publishedBooks);
const publishedBooksSlice = createSlice({
name: 'adminApp/publishedBooks',
initialState,
reducers: {
resetPublishedBooks: (state, action) => initialState,
setLastDocId: {
prepare: doc => {
const payload = doc
return { payload };
},
reducer: (state, action) => {
state.lastDocId = action.payload;
}
},
resetLastDocId: {
prepare: () => {
const payload = null
return { payload };
},
reducer: (state, action) => {
state.lastDocId = action.payload;
}
},
},
extraReducers: {
[getPublishedBooks.fulfilled]: publishedBooksAdapter.setAll,
[getPublishedBooksNext.fulfilled]: publishedBooksAdapter.upsertMany
}
});
export const { resetPublishedBooks, setLastDocId, resetLastDocId } = publishedBooksSlice.actions;
export default publishedBooksSlice.reducer;

lastDocRef only returns the doc reference. You need to get the actual doc itself.
const lastDocRef = await firestoreDB.doc(`catalog/${publishedBooks.lastDocId}`).get();
And you should use await instead of then-catch for more readble code.
export const getPublishedBooksNext = createAsyncThunk('adminApp/publishedBooks/getPublishedBooksNext',
async (params, { dispatch, getState }) => {
const { publishedBooks } = getState().adminApp;
try {
const lastDocRef = await firestoreDB.doc(`catalog/${publishedBooks.lastDocId}`).get();
const snap = await firestoreDB
.collection('catalog')
.orderBy('lastPublish', 'desc')
.startAfter(lastDocRef)
.limit(10)
.get()
const lastDoc = snap.docs[snap.docs.length-1];
let books = [];
dispatch(setLastDocId(lastDoc.id));
snap.forEach(bookDoc => {
const id = bookDoc.id;
const data = bookDoc.data();
const lastPublish = data.lastPublish.toDate().toISOString();
books.push({ ...data, id, lastPublish });
});
return books;
} catch (error) {
return {}
}
}
);
Edit: You can also save the lastDoc to redux then reference it later to avoid additional workload fetching for the lastDocRef.
export const getPublishedBooksNext = createAsyncThunk('adminApp/publishedBooks/getPublishedBooksNext',
async (params, { dispatch, getState }) => {
const { lastDocRef } = getState().adminApp; // get saved lastDoc
try {
const snap = await firestoreDB
.collection('catalog')
.orderBy('lastPublish', 'desc')
.startAfter(lastDocRef) // use it here.
.limit(10)
.get()
const lastDoc = snap.docs[snap.docs.length-1];
let books = [];
// dispatch(setLastDocId(lastDoc.id)); // instead of saving the doc id
dispatch(setLastDoc(lastDoc)); // save the last document instead
snap.forEach(bookDoc => {
const id = bookDoc.id;
const data = bookDoc.data();
const lastPublish = data.lastPublish.toDate().toISOString();
books.push({ ...data, id, lastPublish });
});
return books;
} catch (error) {
return {}
}
}
);

Related

Why can't I access data after fetching?

I'm trying to keep session stayed logged in after refreshing the browser. The user data that is being fetched is not rendering after being fetched. The console is saying "Cannot read properties of undefined (reading 'user'). This is my code for the login/sign up page.
The data I'm trying to access is in the picture below:
(Auth.js)
const Auth = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const [isSignup, setIsSignup] = useState(false);
const [inputs, setInputs] = useState({
name: "",
username: "",
email: "",
password: ""
})
const handleChange = (e) => {
setInputs(prevState => {
return {
...prevState,
[e.target.name]: e.target.value
}
})
}
const sendRequest = async (type = '') => {
const res = await axios.post(`/user/${type}`, {
name: inputs.name,
email: inputs.email,
username: inputs.username,
password: inputs.password,
}).catch(error => console.log(error))
const data = await res.data;
console.log(data)
return data;
}
const handleSubmit = (e) => {
e.preventDefault()
console.log(inputs)
if (isSignup) {
sendRequest("signup")
.then((data) => {
dispatch(authActions.login());
localStorage.setItem('userId', data.user._id);
navigate("/posts");
});
} else {
sendRequest("login")
.then((data) => {
dispatch(authActions.login());
localStorage.setItem('userId', data.user._id);
navigate("/posts");
});
}
}
Redux store file
const authSlice = createSlice({
name: "auth",
initialState: { isLoggedIn: false },
reducers: {
login(state) {
state.isLoggedIn = true
},
logout(state) {
state.isLoggedIn = false
}
}
})
export const authActions = authSlice.actions
export const store = configureStore({
reducer: authSlice.reducer
})
Chaining promises using .then() passes the resolved value from one to the next. With this code...
sendRequest("...")
.then(() => dispatch(authActions.login()))
.then(() => navigate("/posts"))
.then(data => localStorage.setItem('token', data.user))
You're passing the returned / resolved value from navigate("/posts") to the next .then() callback. The navigate() function returns void therefore data will be undefined.
Also, your redux action doesn't return the user so you can't chain from that either.
To access the user data, you need to return it from sendRequest()...
const sendRequest = async (type = "") => {
try {
const { data } = await axios.post(`/user/${type}`, { ...inputs });
console.log("sendRequest", type, data);
return data;
} catch (err) {
console.error("sendRequest", type, err.toJSON());
throw new Error(`sendRequest(${type}) failed`);
}
};
After that, all you really need is this...
sendRequest("...")
.then((data) => {
dispatch(authActions.login());
localStorage.setItem('userId', data.user._id);
navigate("/posts");
});
Since you're using redux, I would highly recommend moving the localStorage part out of your component and into your store as a side-effect.

Multiple axios get request not returning the data properly

I have created a react hook to work on with multiple get request using axios
const useAxiosGetMultiple = (urls,{preventCall = false} = {}) => {
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const [response, setResponse] = useState(()=>{
const temp = {}
Object.keys(urls).forEach(key => temp[key] = [])
return temp
})
const [reloadToken, setReloadToken] = useState(false)
const urlObj = useRef({...urls})
const unmountedOnReload = useRef(false)
useEffect(() => {
if(preventCall === true){
return null
}
let unmounted = false;
const source = axios.CancelToken.source();
setLoading(true)
const requests = []
Object.values(urlObj.current).forEach(url => {
requests.push(
axios.get(url, {
cancelToken: source.token,
})
);
});
const result = {}
const errors = {}
console.log(requests)
Promise.allSettled(requests)
.then(resArray => {
if(!unmounted){
console.log('from promise allsettled')
console.log(resArray)
console.log(urlObj.current)
Object.keys(urlObj.current).forEach((key,i) =>{
if(resArray[i].status === 'fulfilled'){
result[key] = resArray[i].value.data.responseData
}
if(resArray[i].status === 'rejected'){
errors[key] = resArray[i].reason
result[key] = []
}
})
setError(errors)
setLoading(false)
setResponse(result)
}
})
.catch(err => {
if (!unmounted) {
setError(err);
setLoading(false);
setResponse([])
if (axios.isCancel(err)) {
console.log(`request cancelled:${err.message}`);
} else {
console.log("another error happened:" + err.message);
}
}
})
return () => {
unmounted = true;
unmountedOnReload.current = true
source.cancel("Api call cancelled on unmount");
};
}, [reloadToken,preventCall]);
const reFetchAll = () => {
setReloadToken((token) => !token);
};
const reload = (urlKey) =>{
unmountedOnReload.current = false
setLoading(true)
axios.get(urls[urlKey])
.then(res =>{
if(!unmountedOnReload.current){
setLoading(false)
setResponse({...response,[urlKey]: res.data.responseData})
}
})
.catch(err=>{
if(!unmountedOnReload.current){
setLoading(false)
setError({...error, [urlKey]: err})
setResponse({...response,[urlKey]: []})
}
})
}
return {response, loading, error, reFetchAll, reload, setLoading};
};
I call this hook as follows..
const {response,loading,setLoading,reload} = useAxiosGetMultiple({
stateCodes: StateCode.api,
countryCodes: CountryCode.api,
districts: District.api,
})
Rather than getting variable stateCodes containing state codes or countryCodes containing country codes it's returning in wrong order or returning same data in multiple variable. Every time the call happens every time it changes. I also tried axios.all method instead of Promise.all but problem remains same.
Even in chrome's network panel the response data is improper.
What's the possible cause for this error and how to fix it ?
Thanks in advance

useEffect efficiency in Star Wars API

I need some help with me current project making in React. I'am making a star-wars-app for my job interview and I stucked on a one problem.
Fetch efficiency.
I'am fetching this data, and then fetching some more because of the url's in the first fetched data, and everything fetches good, but first i have the 'url's' seeing in the table and then it changes into correct data.
I want to set the 'fetched' state to true when everything is rendered correctly but I don't know how to do it.
const api = `https://swapi.dev/api/people/`;
const [characters, setCharacters] = useState([]);
const [speciesOptions, setSpeciesOptions] = useState([]);
const [homeworldOptions, setHomeworldOptions] = useState([]);
const [fetched, setFetched] = useState(false);
useEffect(() => {
const fetchedTimeout = () => {
setTimeout(() => {
setFetched(true);
}, 2000);
};
const fetchArray = (array, arrName) => {
for (let elem of array) {
fetch(elem).then((response) =>
response.json().then((data) => {
array.shift();
array.push(data.name);
})
);
}
if (arrName === "species") {
if (!array.length) {
array.push("Unspecified");
}
}
};
async function fetchOtherData(characters) {
await characters.forEach((character) => {
const homeworld = character.homeworld;
const vehicles = character.vehicles;
const starships = character.starships;
const species = character.species;
fetch(homeworld).then((response) =>
response.json().then((data) =>
setCharacters((prevData) =>
prevData.map((prevCharacter) =>
prevCharacter.homeworld === homeworld
? {
...prevCharacter,
homeworld: data.name,
}
: prevCharacter
)
)
)
);
fetchArray(vehicles);
fetchArray(starships);
fetchArray(species, "species");
});
await setCharacters(characters);
await fetchedTimeout();
}
const fetchAllCharacters = (allCharacters, data) => {
if (data.next) {
fetch(data.next)
.then((response) => response.json())
.then((data) => {
allCharacters.push(...data.results);
fetchAllCharacters(allCharacters, data);
});
}
if (!data.next) {
fetchOtherData(allCharacters);
}
};
async function fetchApi() {
const allCharacters = [];
await fetch(api)
.then((response) => response.json())
.then((data) => {
allCharacters.push(...data.results);
fetchAllCharacters(allCharacters, data);
})
.catch((error) => console.log(error));
}
const setSpeciesFiltering = () => {
const speciesFiltering = [];
for (let character of characters) {
const characterSpecies = character.species.join();
const foundSpecies = speciesFiltering.indexOf(characterSpecies);
if (foundSpecies === -1) {
speciesFiltering.push(characterSpecies);
}
}
const speciesOptions = speciesFiltering.map((species) => (
<option value={species}>{species}</option>
));
setSpeciesOptions(speciesOptions);
};
const setHomeworldFiltering = () => {
const homeworldFiltering = [];
for (let character of characters) {
const characterHomeworld = character.homeworld;
const foundSpecies =
homeworldFiltering.indexOf(characterHomeworld);
if (foundSpecies === -1) {
homeworldFiltering.push(characterHomeworld);
}
}
const homeworldOptions = homeworldFiltering.map((homeworld) => (
<option value={homeworld}>{homeworld}</option>
));
setHomeworldOptions(homeworldOptions);
};
fetchApi();
setSpeciesFiltering();
setHomeworldFiltering();
}, []);
I will appreciate your response.
Okay, after all the comments (thanks for that!) i changed the code to something like this.
useEffect(() => {
const fetchOtherData = (characters) => {
const charactersWithAllData = [];
characters.forEach((character) => {
const homeworld = character.homeworld;
const species = character.species;
const vehicles = character.vehicles;
const starships = character.starships;
let urls = [homeworld, ...species, ...vehicles, ...starships];
Promise.all(
urls.map((url) => {
if (url.length) {
fetch(url)
.then((response) => response.json())
.then((data) => {
if (url.search("species") > 0) {
character.species = data.name;
}
if (url.search("planets") > 0) {
character.homeworld = data.name;
}
if (url.search("vehicles") > 0) {
character.vehicles.shift();
character.vehicles.push(data.name);
}
if (url.search("starships") > 0) {
character.starships.shift();
character.starships.push(data.name);
}
})
.catch((err) => console.error(err));
}
if (!url.length) {
if (url.search("species")) {
character.species = "Unspecified";
}
if (url.search("vehicles")) {
character.vehicles = "";
}
if (url.search("starships")) {
character.starships = "";
}
}
})
).then(charactersWithAllData.push(character));
});
return charactersWithAllData;
};
const fetchApi = () => {
const characters = [];
Promise.all(
[api].map((api) =>
fetch(api)
.then((response) => response.json())
.then((data) => characters.push(...data.results))
.then((data) => {
setCharacters(fetchOtherData(characters));
})
)
);
};
fetchApi();
}, []);
In what point do i have to set the 'characters' state ? Because in the situation above the data first shows on the screen, and then the state is set.
As other comments say, using Promise.all and refactoroing your useEffect is best solution for this.
But this might be helpful if you don't want to change a lot.
(but still consider refactor your hook)
const [loading, setLoading] = useState(0);
const isLoading = loading > 0;
// replace your fetches with below:
const myFetch = async (path) => {
try {
setLoading(loading => loading + 1);
return await fetch(path);
} finally {
setLoading(loading => loading - 1);
}
};
useEffect(() => {
// do your stuffs
}, []);

Redux Thunk data is stored as a string not as an object

I want store a new group as an object into the groups entity in the store. Everything works perfectly but the new group is stored as an object not as a string. I am using Mockoon to mock an API and the data type is set to be application/json. Can someone explain to me what might be the possible cause of this behavior? I am quite new on using redux so some input would be really appreciated too.
Thank you
const dispatch = useDispatch();
const initialGroupState = {
id: null,
name: "",
description: "",
members: []
}
const [group, setGroup] = useState(initialGroupState)
const [submitted, setSubmitted] = useState(false);
const handleInputChange = event => {
const { name, value } = event.target;
setGroup({ ...group, [name]: value });
};
const saveGroup = (e) => {
e.preventDefault();
const {name, description} = group;
dispatch(createGroup(name, description))
.then(data => {
setGroup({
id: Math.floor(Math.random() * 10000),
name: data.name,
description: data.description,
});
setSubmitted(true);
})
.catch(e => {
console.log(e);
});
}
const newGroup = () => {
setSubmitted(false);
};
My reducer:
const initialState = []
function groupsReducer(groups = initialState, action) {
const { type, payload } = action;
console.log([...groups]);
switch (type) {
case CREATE_GROUP:
return [...groups, payload];
case RETRIEVE_GROUPS:
return payload;
default:
return groups;
}
};
My actions:
export const createGroup = (name, description) => async (dispatch) => {
try {
const res = await GroupDataService.create({ name, description });
dispatch({
type: CREATE_GROUP,
payload: res.data,
});
console.log(res.data)
return Promise.resolve(res.data);
} catch (err) {
console.log(err)
return Promise.reject(err);
}
};

Firestore cloud function to recursively update subcollection/collectionGroup

I have this cloud function:
import pLimit from "p-limit";
const syncNotificationsAvatar = async (
userId: string,
change: Change<DocumentSnapshot>
) => {
if (!change.before.get("published") || !change.after.exists) {
return;
}
const before: Profile = change.before.data() as any;
const after: Profile = change.after.data() as any;
const keysToCompare: (keyof Profile)[] = ["avatar"];
if (
arraysEqual(
keysToCompare.map((k) => before[k]),
keysToCompare.map((k) => after[k])
)
) {
return;
}
const limit = pLimit(1000);
const input = [
limit(async () => {
const notifications = await admin
.firestore()
.collectionGroup("notifications")
.where("userId", "==", userId)
.limit(1000)
.get()
await Promise.all(
chunk(notifications.docs, 500).map(
async (docs: admin.firestore.QueryDocumentSnapshot[]) => {
const batch = admin.firestore().batch();
for (const doc of docs) {
batch.update(doc.ref, {
avatar: after.avatar
});
}
await batch.commit();
}
)
);
})
];
return await Promise.all(input);
};
How can I recursively update the notifications collection but first limit the query to 1.000 documents (until there are not more documents) and then batch.update them? I'm afraid this query will timeout since collection could grow big over time.
Posting a solution I worked out, not following the context of the question though but it can easily be combined. Hope it helps someone else.
import * as admin from "firebase-admin";
const onResults = async (
query: admin.firestore.Query,
action: (batch: number, docs: admin.firestore.QueryDocumentSnapshot[]) => Promise<void>
) => {
let batch = 0;
const recursion = async (start?: admin.firestore.DocumentSnapshot) => {
const { docs, empty } = await (start == null
? query.get()
: query.startAfter(start).get());
if (empty) {
return;
}
batch++;
await action(
batch,
docs.filter((d) => d.exists)
).catch((e) => console.error(e));
await recursion(docs[docs.length - 1]);
};
await recursion();
};
const getMessages = async () => {
const query = admin
.firestore()
.collection("messages")
.where("createdAt", ">", new Date("2020-05-04T00:00:00Z"))
.limit(200);
const messages: FirebaseFirestore.DocumentData[] = [];
await onResults(query, async (batch, docs) => {
console.log(`Getting Message: ${batch * 200}`);
docs.forEach((doc) => {
messages.push(doc.data());
});
});
return messages;
};

Categories