Firestore data doesn't show up - javascript

In my custom hook, I am getting all the images in firestore collection. That was working fine UNTIL I have tried to get also metadata's of images. When I added the getMetaData function, I can't set the data to state even the datas seem fine in an array.
See the code please.
import { useState, useEffect } from "react";
import { firebaseFireStore } from "../firebase/config";
import { storage } from "../firebase/config";
import { ref, getMetadata } from "firebase/storage";
const useFirestore = (collection) => {
const [docs, setDocs] = useState([]);
useEffect(() => {
const unsub = firebaseFireStore
.collection(collection)
.onSnapshot((snap) => {
let documents = [];
snap.forEach((doc) => {
const forestRef = ref(storage, doc.data().url);
getMetadata(forestRef)
.then((metadata) => {
documents.push({ ...doc.data(), metadata, id: doc.id });
return documents;
})
.catch((error) => {
console.log(error);
});
});
setDocs(documents);
console.log(docs); // [] EMPTY ARRAY, PROBLEM IS HERE.
console.log(documents); // [] length:13 data is complete inside.
});
return () => unsub();
// this is a cleanup function that react will run when
// a component using the hook unmounts
}, []);
return { docs };
};
export default useFirestore;
So, even if documents array has the data with metadata in it, it can't be set to state of docs.
Any help appreciated.

You can't log the docs state right after enqueueing a state update since React asynchronously processes the state updates. But this isn't exactly your issue or what you are actually trying to solve. The code is mapping over the snapshot and enqueueing an asynchronous request to retrieve metadata and asynchronously mutating the documents array that was updated in React a long* time ago. The console.log(documents) is just exposing this mutation later when the browser is processing the log buffer.
To resolve I suggest declaring the snapshot.forEach callback async so it can wait for the getMetadata call to resolve. Use a functional state update to shallow merge each document as it resolves with metadata.
Example:
useEffect(() => {
const unsubscribe = firebaseFireStore
.collection(collection)
.onSnapshot((snap) => {
snap.forEach(async (doc) => {
try {
const forestRef = ref(storage, doc.data().url);
const metadata = await getMetadata(forestRef);
setDocs(documents => [
...documents,
{ ...doc.data(), metadata, id: doc.id }
]);
} catch(error) {
console.log(error);
};
});
});
return unsubscribe;
}, []);
* "Long time ago" -> some previous render cycle.

Related

useEffect hook runs infinitely when used in a custom hook

Below is my custom hook, I'm trying to handle everything from the hook. I have seen similar questions but none seems to work for my case and I have been made to believe there's a solution for this approach, jus can't figure it out.
const useResource = (baseUrl) => {
const [resources, setResources] = useState([]);
const create = async (resource) => {
const response = await axios.post(baseUrl, resource)
setResources(resources.concat(response.data));
console.log(resources)
return response.data
}
const get = async () => {
const response = await axios.get(baseUrl);
setResources(response.data)
return response.data
}
const service = {
create,
get
}
return [
resources, service
]
}
Here is my approach to use the custom hook, but request keeps looping nonstop, please how do I stop it running after every render?
const App = () => {
const content = useField('text');
const name = useField('text');
const number = useField('text');
const [notes, noteService] = useResource('http://localhost:3005/notes');
const [persons, personService] = useResource('http://localhost:3005/persons');
useEffect(() => {
noteService.get();
}, [noteService])
useEffect(() => {
personService.get();
}, [personService])
const handleNoteSubmit = (event) => {
event.preventDefault();
noteService.create({ content: content.value });
}
const handlePersonSubmit = (event) => {
event.preventDefault();
personService.create({ name: name.value, number: number.value});
}
Edit: I just had to disable ESLint for the dependency line, because I just need it to run once after every render. Works well!
useEffect(() => {
noteService.get();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
personService.get();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
As correctly pointed out in comments, each time the component renders and calls your useResource hook, a new service object is created. If this service object is used as a dependency for any other hooks this will trigger their callbacks.
The solution is to memoize the service object so it's being provided as a stable reference. This can be accomplished via the useMemo hook. Because service will be memoized, the create callback will also be memoized and contain stale resources state. To address this update create to use a functional state update when appending new response data to the existing state.
Example:
import { useEffect, useMemo, useState } from 'react';
const useResource = (baseUrl) => {
const [resources, setResources] = useState([]);
const create = async (resource) => {
const response = await axios.post(baseUrl, resource);
// Functional update to update from previous state
setResources(resources => resources.concat(response.data));
return response.data;
}
const get = async () => {
const response = await axios.get(baseUrl);
setResources(response.data);
return response.data;
}
const service = useMemo(() => ({
create,
get
}), []);
return [resources, service];
};

Update a Table with React Hooks when a Row is Added, Deleted and Modified? [Issue: Gets Before Post and Delete]

I'm using Axios to get, put, and delete values from our database and have them displayed in a table; however, I need to refresh the page to see my changes. To find answers, I've visited these posts: How to update the page after call Axios Successful ? React, refresh table after action in react, and How can I use React Hooks to refresh a table when a row is deleted or added?
Unfortunately, I am still stuck and unsure how to dynamically update table rows upon response updates.
Update: I have noticed that the getValues function runs prior to the post and delete methods, which is why it is currently showing the previous values before the methods execute.
Axios.js - Where I am using get and delete methods. They work as I've had responses printed on the console.
import axios from "axios";
const getValues = async () => {
const values = await axios
.get("https://pokeapi.co/api/v2/type/")
.then((response) => {
return response.data;
})
.catch(function (error) {
console.log(error);
});
return values;
};
const postValues = (values) => {
axios
.post("https://pokeapi.co/api/v2/type/")
.then((response) => {
console.log("Post Values: ", response.data);
return response.data;
});
};
const deleteValues = (id) => {
console.log(id);
const deleteValues = axios
.delete(`https://pokeapi.co/api/v2/type/${id}`)
.then((response) => {
console.log("Delete Values: ", response);
})
.catch(function (error) {
console.log(error);
});
return deleteValues;
};
export { getValues, postValues, deleteValues }
ValuesTable.js - Where the delete method executes
import Axios from "./Axios";
const [data, setData] = React.useState();
useEffect(() => {
Axios.getValues().then((result) => {
setData(result.data);
});
}, [data]);
return (
{data.map((values) => {
<TableRow/>
<TableCell>{values.values}</TableCell>
<TableCell>
<Button
onClick={() =>
Axios.deleteValues(values.id);
}
/>
})};
)
Form.js - Where the post method executes
if (values.id === 0) {
Axios.postValues(values);
} else {
Axios.putValues(values, values.id);
}
UseState setData(result.data) loads all the existing values in the database.
Method deleteValues deletes a value in an array.
Method postValues adds a value into the database.
Well, you don't what to unconditionally call setData within an useEffect hook with data as a dependency as this will cause an infinite loop (render looping) to occur.
Since the getValues utility already unpacks the response.data value there is likely no need to do it again in your UI. Also, remove the data dependency.
useEffect(() => {
Axios.getValues()
.then((result) => {
setData(result.results);
});
}, []);
For the deleteValues utility, if console.log("Delete Values: ", response); is showing the correct values than I think you need to return this value from deleteValues.
const deleteValues = (id) => {
console.log(id);
const deleteValues = axios
.delete("https://pokeapi.co/api/v2/type/${id}`)
.then((response) => {
console.log("Delete Values: ", response);
return response; // <-- new data values
})
.catch(function (error) {
console.log(error);
});
return deleteValues;
};
Then in ValuesTable you need to update your data state with the new deleted values.
{data.map((values) => {
...
<Button
onClick={() => {
Axios.deleteValues(values.id)
.then(data => setData(data));
}}
/>
...
})};
Update
Ok, since the deleteValues utility doesn't return the updated data from the backend you will need to maintain your local state manually. I suggest doing this work in a callback handler. Upon successful deletion, update the local state.
const [data, setData] = React.useState();
useEffect(() => {
Axios.getValues().then((result) => {
setData(result.data);
});
}, []);
const deleteHandler = id => async () => {
try {
await Axios.deleteValues(id); // no error, assume success
setData(data => data.filter((item) => item.id !== id));
} catch(err) {
// whatever you want to do with error
}
};
return (
...
{data.map((values) => {
<TableRow/>
<TableCell>{values.values}</TableCell>
<TableCell>
<Button onClick={deleteHandler(values.id)}>
Delete
</Button>
})};
...
)
Note that I've written deleteHandler to be a curried function so you don't need an anonymous callback function for the button's onClick handler. It encloses the current id in an "instance" of the callback.
Update 2
If you are making a lot of different changes to your data in the backend it may just be easier to use a "fetch" state trigger to just refetch ("get") your data after each backend update. Anytime you make a call to update data in your DB, upon success trigger the fetch/refetch via a useEffect hook.
const [data, setData] = React.useState();
const [fetchData, setFetchData] = useState(true);
const triggerDataFetch = () => setFetchData(t => !t);
useEffect(() => {
Axios.getValues().then((result) => {
setData(result.data);
});
}, [fetchData]);
const deleteHandler = id => async () => {
try {
await Axios.deleteValues(id); // no error, assume success
triggerDataFetch(); // <-- trigger refetch
} catch(err) {
// whatever you want to do with error
}
};
I think you wrong in here :
useEffect(() => {
Axios.getValues().then((result) => {
setData(result.data); // result has no data property
});
}, [data]);
Please try change to this
useEffect(() => {
Axios.getValues().then((result) => {
console.log("RESULT",result); // check the actually response from API
setData(result.results); // the response of data is called results
});
}, [data]);
import axios from "axios"; const getValues = async () => { const values = await axios .get("https://pokeapi.co/api/v2/type/")
.then((response) => { return response.data; }) .catch(function (error)
{ console.log(error); }); return values; };
I don't know why but what are you trying to achieve with this. You should either use async/await clause or then clause but you are using both atleast have some good practice of coding first.
Second I think you should use async await inside try catch and remove then/catch phrases to make your code more understandable, then if you store your result inside values then simply return values.data and your problem might be resolved.
Since the deleteValues function deletes a specific object from the array on the server-side, I have decided to filter the list of objects in the array in order to remove the matching id to reflect on the front end. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
This is how I approached it.
{data.map((values) => {
...
<Button
onClick={() => {
setData(data.filter((item) => item.id !== values.id)); // <- Filter
Axios.deleteValues(values.id)
.then(data => setData(data));
}}
/>
...
})};

async/await function not always behave correctly

I'm developing a react-native/nodeJS project and I'm experiencing issues with the Axios API call to my backend using async/await functions.
Here's the code:
const TimeTable = () => {
const [attendedCourses, setAttendedCourses] = useState([]);
const [courseSchedules, setCourseSchedules] = useState([]);
useEffect(() => {
getUserCourses();
getCourseSchedule();
console.log(courseSchedules);
}, []);
const getCourseSchedule = async () => {
for (const item of attendedCourses) {
try {
const res = await axios.get(`/api/lesson/findById/${item.courseId}`);
setCourseSchedules((prev) => [
...prev,
{
id: res.data._id,
name: res.data.name,
schedule: [...res.data.schedule],
},
]);
} catch (err) {
const error = err.response.data.msg;
console.log(error);
}
}
};
const getUserCourses = async () => {
const userId = "12345678"; //hardcoded for testing purpose
try {
const res = await axios.get(`/api/users/lessons/${userId}`);
setAttendedCourses(res.data);
} catch (err) {
const error = err.response.data.msg;
console.log(error);
}
};
return (...); //not important
};
export default TimeTable;
The method getUserCourses() behave correctly and returns always an array of objects which is saved in the attendedCourses state. The second method getCourseSchedule() doesn't work correctly. The console.log() in the useEffect() prints most of the time an empty array.
Can someone please explain to me why? Thank you!
While the method is async, the actual useEffect is not dealing it in asynchronous manner and won't await till you reach the console.log in the useEffect. If you put the console.log inside the getCourseSchedule method and log the result after the await, it'll show you correct result every time.
You are confusing the async nature of each method. Your code does not await in the useEffect, it awaits in the actual method while the rest of the useEffect keeps executing.
If you really want to see the result in useEffect, try doing:
useEffect(() => {
const apiCalls = async () => {
await getUserCourses();
await getCourseSchedule();
console.log(courseSchedules);
}
apiCalls()
})
Your useEffect has an empty array as dependencies that means it is run only onetime in before initial render when the courseSchedules has initial value (empty array)
To see the courseSchedules when it change you should add another useEffect like this:
useEffect(() => {
console.log(courseSchedules);
}, [courseSchedules]);

Using react-redux and redux-thunk for loading indicator

I am working a project which use react-redux and redux-thunk for development. Previously the approach was to check api calls one by one and manually check if all the data is being fetched. As I found that after v7.0, batch is introduced in react-redux to help solving the issue. But the page also requires loading indicator as well.
The current approach is having several dispatches in batch to reduce unnecessary re-rendering, and manually check if all the data is fetched in the render, but I was wondering if there is any other method that can be applied on the batch to cut some hard code check.
Here is the current sample code:
// in action file
...
function fetchSomeData() {
// call api to store data
return dispatch => {
batch(() => {
dispatch(fetchData1());
dispatch(fetchData2());
dispatch(fetchData3());
..some more dispatches...
});
}
}
...
// in react component
dataLoaded(){
....retrieve all the data from different places...
if (!data1) return false;
if (!data2) return false;
...check all the data...
return true;
}
...
render() {
if (this.dataLoaded()) {
return actual_content;
} else {
return loading_content;
}
}
...
I tried to directly use then, and create another method, return batch, call fetchSomeData, then use then(), but all produce "Cannot read property 'then' of undefined" error.
I also used Promise.all, but with no luck. Use of Promise.all is shown as below:
function fetchSomeData() {
// call api to store data
return dispatch => {
Promise.all([
dispatch(fetchData1());
dispatch(fetchData2());
dispatch(fetchData3());
..some more dispatches...
])
.then(() => dispatch(setLoading(false)));
}
}
I also checked other posts on the stackoverflow, but many posts suggest other middleware, and additional dependency requires approval as one of the requirement is limited bandwidth, use minimal dependencies as needed.
Redux Toolkit actually helped me to resolve this issue. A sample code looks like:
userSlice
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit';
import axios from 'axios';
export const userSlice = createSlice({
name: 'user',
initialState: {
data: [],
isLoading: false,
error: null,
},
extraReducers(builder) {
builder.addCase(fetchUsers.pending, (state, action) => {
state.isLoading = true;
});
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.isLoading = false;
state.data = action.payload;
});
builder.addCase(fetchUsers.rejected, (state, action) => {
state.isLoading = false;
state.error = action.error;
});
},
});
export const fetchUsers = createAsyncThunk('users/fetch', async () => {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
console.log(new Date());
return response.data;
});
export const usersReducer = userSlice.reducer;
postSlice
// similar configuration as user
function later(delay) {
return new Promise(function (resolve) {
setTimeout(resolve, delay);
});
}
export const fetchPosts = createAsyncThunk('posts/fetch', async () => {
await later(5000);
const response = await axios.get(
'https://jsonplaceholder.typicode.com/posts'
);
console.log(new Date());
return response.data;
});
third slice to call the thunks
This action doesn't have to be async thunk, writing a custom thunk should also work.
// similar configuration as previous
export const fetchHome = createAsyncThunk(
'home/fetch',
async (_, thunkAPI) => {
const res = await Promise.all([
thunkAPI.dispatch(fetchUsers()),
thunkAPI.dispatch(fetchPosts()),
]);
console.log(res);
return [];
}
);
The result looks like:
The fetch home thunk waited until all the async thunks have been resolved and then emit the final result.
reference: Dispatch action on the createAsyncThunk?

How to Cancel subscription in async promise to avoid memory leak in Reactjs

Within my React component, I have an async request which dispatches an action to my Redux store which is called within the useEffect hook:
const loadFields = async () => {
setIsLoading(true);
try {
await dispatch(fieldsActions.fetchFields(user.client.id));
} catch (error) {
setHasError(true);
}
setIsLoading(false);
}
useEffect(() => { if(isOnline) { loadFields() } }, [dispatch, isOnline]);
The action requests data via a fetch request:
export const fetchFields = clientId => {
return async dispatch => {
try {
const response = await fetch(
Api.baseUrl + clientId + '/fields',
{ headers: { 'Apiauthorization': Api.token } }
);
if (!response.ok) {
throw new Error('Something went wrong!');
}
const resData = await response.json();
dispatch({ type: SET_FIELDS, payload: resData.data });
} catch (error) {
throw error;
}
}
};
export const setFields = fields => ({
type : SET_FIELDS,
payload : fields
});
When this is rendered within the React app it results in the following warning:
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in %s.%s, a useEffect cleanup function
I believe this occurs because the promise doesn't have a "clean-up" function. But I am unsure where to place this? Should I have some logic within LoadFields()? Or must this be done within the useEffect hook?
This tutorial which will help you to resolve your issue.
Quick example: with Promises
function BananaComponent() {
const [bananas, setBananas] = React.useState([])
React.useEffect(() => {
let isSubscribed = true
fetchBananas().then( bananas => {
if (isSubscribed) {
setBananas(bananas)
}
})
return () => isSubscribed = false
}, []);
return (
<ul>
{bananas.map(banana => <li>{banana}</li>)}
</ul>
)
}
Quick example: with async/await (Not the best one but that should work with an anonymous function)
function BananaComponent() {
const [bananas, setBananas] = React.useState([])
React.useEffect(() => {
let isSubscribed = true
async () => {
const bananas = await fetchBananas();
if (isSubscribed) {
setBananas(bananas)
}
})();
return () => isSubscribed = false
}, []);
return (
<ul>
{bananas.map(banana => <li>{banana}</li>)}
</ul>
)
}
First issue
If your useEffect() fetches data acynchronously then it would be a very good idea to have a cleanup function to cancel the non-completed fetch. Otherwise what could happen is like that: fetch takes longer than expected, meantime the component is re-rendered for whatever reason. Maybe because its parent is re-rendered. The cleanup of useEffect runs before re-render and the useEffect itself runs after re-render. To avoid having another fetch inflight it's better to cancel the previous one. Sample code:
const [data, setData] = useState();
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const apiData = await fetch("https://<yourdomain>/<api-path>",
{ signal: controller.signal });
setData(apiData);
} catch (err) {
if (err.name === 'AbortError') {
console.log("Request aborted");
return;
}
}
};
fetchData();
return () => {
controller.abort();
}
});
Second issue
This code
return async dispatch => {
will not work because neither dispatch nor Redux store support async actions. The most flexible and powerful way to handle this issue is to use middleware like redux-saga. The middleware lets you:
dispatch 'usual' sync actions to Redux store.
intercept those sync actions and in response make one or several async calls doing whatever you want.
wait until async call(s) finish and in response dispatch one or several sync actions to Redux store, either the original ones which you intercepted or different ones.

Categories

Resources