My application uses the eBay API and allows a user to save a list of favorite items.
I'm storing the user's Favorites data which includes the item ID for a product in a user field in my database.
I need to display the saved favorites yet with updated information from the eBay API, since the data saved (such as price, number of bids) is saved statically when a user selects a Favorite.
I've set up a separate action creator called getUpdates to the database which makes a call like this, and the call works fine.
ebayapi.com/itemID=id2342342,id3234234,id82829294,id234234234
In my Favorites component, i begin by making a call to the database to get the user's favorite items.
useEffect(() => {
props.getFavorites(props.loggedUser.id);
}, [props.loggedUser.id]);
This successfully returns and established my favorites state, which includes a list of the user's favorite items.
I'm trying to take each of the itemId's from the favorites state to pass to my ebay api to retrieve the updated data for each item.
Now, when I do something like what is shown below with a call to my getUpdates action creator...i'm getting an infinite loop, although it is storing the data properly in a new state field that I've defined favUpdates
const Favorites = props => {
useEffect(() => {
props.getFavorites(props.loggedUser.id);
}, [props.loggedUser.id]);
useEffect(() => {
setData(props.cardsToShow);
}, [props]);
const mapFAVS = props.favorites;
const data = Array.from(mapFAVS); //the favorites state is an array of objects so transforming data
const updatedFavs = data.map(item => item.id); //strips out the item id's
const formatFavs = updatedFavs.map(id => id.join(","));
props.getUpdates(formatFavs); //updates favUpdates state
I tried to change this so that the getUpdates call was within one of the useEffect hooks, like this...which also causes an infinite loop.
const Favorites = props => {
useEffect(() => {
props.getFavorites(props.loggedUser.id);
}, [props.loggedUser.id]);
useEffect(() => {
setData(props.cardsToShow);
}, [props]);
const mapFAVS = props.favorites;
const data = Array.from(mapFAVS);
const updatedFavs = data.map(item => item.id);
const formatFavs = updatedFavs.map(id => id.join(","));
useEffect(() => {
props.getUpdates(formatFavs);
}, [props]);
If i remove the second props argument the action creator does not get called and there is an axios error on the backend. Any ideas?
This was solved by adjusting the second argument in the useEffect hook.
useEffect(() => {
props.getUpdates(formatFavs);
}, [props.favorites]);
Related
I am trying to learn firestore realtime functionality.
Here is my code where I fetch the data:
useEffect(() => {
let temp = [];
db.collection("users")
.doc(userId)
.onSnapshot((docs) => {
for (let t in docs.data().contacts) {
temp.push(docs.data().contacts[t]);
}
setContactArr(temp);
});
}, []);
Here is my database structure:
When I change the data in the database I am unable to see the change in realtime. I have to refresh the window to see the change.
Please guide me on what I am doing wrong.
Few issues with your useEffect hook:
You declared the temp array in the way that the array reference is persistent, setting data with setter function from useState requires the reference to be new in order to detect changes. So your temp array is updated (in a wrong way btw, you need to cleanup it due to now it will have duplicates) but React is not detectign changes due to the reference to array is not changed.
You are missing userId in the dependency array of useEffect. If userId is changed - you will continue getting the values for old userId.
onSnapshot returns the unsubscribe method, you have to call it on component unMount (or on deps array change) in order to stop this onSnapshot, or it will continue to work and it will be a leak.
useEffect(() => {
// no need to continue if userId is undefined or null
// (or '0' but i guess it is a string in your case)
if (!userId) return;
const unsub = db
.collection("users")
.doc(userId)
.onSnapshot((docs) => {
const newItems = Object.entries(
docs.data().contacts
).map(([key, values]) => ({ id: key, ...values }));
setContactArr(newItems);
});
// cleanup function
return () => {
unsub(); // unsubscribe
setContactArr([]); // clear contacts data (in case userId changed)
};
}, [userId]); // added userId
I am trying to fetch data from firebase. The authData array is empty when the app renders the first time but when it renders multiple times the authData array picks some data.
//import firebase realtime database method
import{db} from './firebase-config';
//handling the posts from the database
const [posts, setPosts]= useState([]);
//handling the user information
const [authData, setAuthData] = useState([])
useEffect(() => {
//fetching posts from the database
db.collection('posts').onSnapshot(snapshot => {
setPosts(snapshot.docs.map(doc => ({
id: doc.id,
post: doc.data()
})))
});
//fetching the users information
db.collection('users').onSnapshot(snapshot=>{
setAuthData(snapshot.docs.forEach((doc)=>({
id: doc.id,
authData: doc.data()
})))
console.log(authData)
})
},[])
That is the expected behavior. If you look at how you define your state:
const [authData, setAuthData] = useState([])
That [] in there is the initial value for the authData state.
Since loading data from the database takes a bit of time, the UI will initially render with the empty array you specify. Then once the data is loaded, and you call setAuthData it will render with the actual data.
If you don't want to render the component until the data is loaded, you could specify null as the default value and then detect that in your rendering code.
I have a file that stores an array of objects. I have a component that fetches data from this file then render the list. The file could be updated somewhere else, I need the component to be updated if the file is modified. I have following code example
const header = () => {
const [list, setList] = useState([]);
// fetch
useEffect(() => {
const loadList = async () => {
const tempList = await getList("/getlist"); // get call to fetch data from file
setList(tempList);
};
loadList ();
}, [list]);
// function to render content
const renderList = () => {
return list.map(obj => (
<div key={obj.name}>
{obj.name}
</div>
));
};
return (
<div>{renderList()}</div>
)
}
// get call
router.get('/getlist',
asyncWrapper(async (req, res) => {
const result = await getList();
res.status(200).json(result).end();
})
);
const getList= async () => {
const list = JSON.parse(await fs.readFile(listPath));
return list;
}
Code has been simplified. If I remove the list from useEffect, then it will only render once and will never update unless I refresh the page. If I include list there, loadList() will get called constantly, and component will get re-rendered again and again. This is not the behavior I want. I am just wondering without making header component async component, how do I only re-render this component when the file is changed?
Thank you very much.
There are two approaches you can take to this:
Polling
Request the URL on an interval, and clear it when the component is unmounted.
Replace loadList () with:
const interval = setInterval(loadList, 60000); // Adjust interval as desired
return () => clearInterval(interval)
Make sure the cache control headers set in the response to /getlist don't stop the browser from noticing updates.
Server push
Rip out your current code to get the data and replace it with something using websockets, possibly via Socket.IO. (There are plenty of tutorials for using Socket.io with React that can be found with Google, but its rather too involved to be part of a SO answer).
INTRODUCTION
I have a list of items with the prop "date" stored on my FireStore. In the client code, I have a FlatList with all of those items ordered by "date" (the first element is the most recent item, the second one, the item I uploaded before the element which appears first, ...)
The problem is that I only get 5 items (but it is because I don't want to get 100 items at once), and I don't know how to combine this with the FlatList's onEndReached (as it is a listener agent that has to be detached when component unmounts) to get more items following the same order.
Any ideas how to make this work? I have commented "<------------" on the lines of the code that I might have to change.
FIRESTORE DATABASE
Items -> user.uid -> userItems:
{
...
date: 1/1/1970
},
{
...
date: 2/1/1970
},
...
{
...
date: 31/1/1970
}
HOW MY FLATLIST HAS TO BE RENDERED:
FlatList items in order:
{ // The most recent one appears at the top of the list
...
date: 31/1/1970
},
...
{
...
date: 2/1/1970
},
{
...
date: 1/1/1970
},
CODE
const [startItem, setStartItem] = useState(null);
useEffect(() => {
const { firebase } = props;
let itemsArray = [];
// Realtime database listener
const unsuscribe = firebase // <------- With this I get the 5 most recent items when component mounts, or only one if the user has uploaded it after the component mounts
.getDatabase()
.collection("items")
.doc(firebase.getCurrentUser().uid)
.collection("userItems")
.orderBy("date") // Sorted by upload date <------------------
.startAfter(startItem && startItem.date) // <-----------------------
.limitToLast(5) // To avoid getting all items at once, we limit the fetch to 5 items <----------
.onSnapshot((snapshot) => {
let changes = snapshot.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
// Get the new item
const newItem = change.doc.data();
// Add the new item to the items list
itemsArray.unshift(newItem);
}
});
// Reversed order so that the last item is at the top of the list
setItems([...itemsArray]); // Shallow copy of the existing array -> Re-render when new items added
setIsLoading(false);
// Change the start item
setStartItem(itemsArray[itemsArray.length - 1]);
});
return () => {
// Detach the listening agent
unsuscribe();
};
}, []);
...
<CardList data={items} isLoading={isLoading} onEndReached={/*how to call the function 'unsuscribe'? */} /> // <----------
What I need is to get the other next 5 more recent items when the end of the list is reached, and then, add them to the bottom of the list
UPDATE (My best approach for now)
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [start, setStart] = useState(null);
const limitItems = 5;
const getItems = () => {
/*
This function gets the initial amount of items and returns a
real time database listener (useful when a new item is uploaded)
*/
const { firebase } = props;
// Return the realtime database listener
return firebase
.getDatabase()
.collection("items")
.doc(firebase.getCurrentUser().uid)
.collection("userItems")
.orderBy("date") // Sorted by upload date
.startAt(start)
.limitToLast(limitItems)
.onSnapshot((snapshot) => {
let changes = snapshot.docChanges();
let itemsArray = [...items]; // <------- Think the error is here
console.log(`Actual items length: ${itemsArray.length}`); // <-- Always 0 WHY?
console.log(`Fetched items: ${changes.length}`); // 5 the first time, 1 when a new item is uploaded
changes.forEach((change) => {
if (change.type === "added") {
// Get the new fetched item
const newItem = change.doc.data();
// Add the new fetched item to the head of the items list
itemsArray.unshift(newItem);
}
});
// The last item is at the top of the list
setItems([...itemsArray]); // Shallow copy of the existing array -> Re-render when new items added
// Stop loading
setIsLoading(false);
// If this is the first fetch...
if (!start && itemsArray.length) {
// Save the startAt snapshot
setStart(itemsArray[itemsArray.length - 1].date);
}
});
};
const getMoreItems = () => {
/*
This funciton gets the next amount of items
and is executed when the end of the FlatList is reached
*/
const { firebase } = props;
// Start loading
setIsLoading(true);
firebase
.getDatabase()
.collection("items")
.doc(firebase.getCurrentUser().uid)
.collection("userItems")
.orderBy("date", "desc")
.startAfter(start)
.limit(limitItems)
.get()
.then((snapshot) => {
let itemsArray = [...items];
snapshot.forEach((doc) => {
// Get the new fethed item
const newItem = doc.data();
// Push the new fetched item to tail of the items array
itemsArray.push(newItem);
});
// The new fetched items will be at the bottom of the list
setItems([...itemsArray]); // Shallow copy of the existing array -> Re-render when new items added
// Stop loading
setIsLoading(false);
// Save the startAt snapshot everytime this method is executed
setStart(itemsArray[itemsArray.length - 1].date);
});
};
useEffect(() => {
// Get a initial amount of items and create a real time database listener
const unsuscribe = getItems();
return () => {
// Detach the listening agent
unsuscribe();
};
}, []);
With this code I can fetch an initial amount of items the first time, and then the next amount when I reach the end of my FlatList. But for some reason the state is not updated inside the listener... so when a new item is uploaded, all the items I got before disapears from the FlatList and they are fethed again when the end of the FlatList is reached.
Okey, after some hours coding I have found a solution. I think this is not the best because it will be better to use the onSnapshot also when the end of the FlatList is reached, but I don't know if this is possible with the Firestore's onSnapshot implementation.
The solution is based on "my best approach" code which is in the question.
Algorithm:
Just, at a first time, I create the Real-time Database Listener, which just does an onSnapshot and then call my function onItemsCollectionUpdate (passing the snapshot as argument), which can perfectly access the updated state of the app (as it is not inside the listener agent)
When we are in the onItemsCollectionUpdate, we just get the items from the snapshot and add them to the items state.
When the end of the FlatList is reached, we just call the function "getItems", which does an static retrieve of the Firestore data (I mean, using the get method from Firebase) and add it to the items state.
When component unmounts, detach the listener agent.
I am writing a application in React-Redux with Firebase Realtime database for storage and wish load initial data while also receiving updates when data changes. The desired data workflow would be:
Load initial data from Firebase Realtime Database
Populate Redux Store and load UI
Receive database update from Firebase Realtime Database
Update Redux store and components
What I have completed so far is step 1 and 2, now I am considering what is best practice for data update on database change.
In my present code I call a series of api functions to retrieve the inital data.
const setInitialStore = () => {
return dispatch => Promise.all([
dispatch(startSetUser()),
dispatch(startSetNotes()),
...
])
}
//** ACTION ***
export const setNotes = (notes) => ({
type: SET_NOTES,
notes
})
//async action, fetch data and dispatches SET_NOTES
export const startSetNotes = () => {
//thunk gives access to store-methods dispatch and getState
return (dispatch, getState) => {
const apiInstruction = { action: DB_ACTION_GET_SUBSCRIBE, payload: null, uid: getState().auth.uid }
//returns a promise
return noteAPI(apiInstruction)
.then((notes) => {
//get data into redux
dispatch(setNotes(notes))
})
}
}
//** REDUCER
const notesReducerDefaultState = []
const notesReducer = (state = notesReducerDefaultState, action) => {
switch (action.type) {
case SET_NOTES:
//returning new array, don't care about what's instate
return action.notes;
...
}
}
//** noteAPI - action "DB_ACTION_GET_SUBSCRIBE" section ***
database.ref(`${dbPath}`).on('value', (snapshot) => {
const notes = []
//parse data
snapshot.forEach((childSnapshot) => {
const note = {
id: childSnapshot.key,
...childSnapshot.val()
}
const noteModel = new NoteModel(note, uid)
notes.push(noteModel.reduxStoreObj)
})
//return notes
resolve(notes)
})
The above code structure explained below.
setInitialStore - dispatches the action from "setNote"
startSetNote - calls the API that fetches data for action generator
noteAPI - Calls Firebase Realtime Database ".on()" and returns all notes
setNote - Action generator called with collection from database
This approach has one crucial flaw though - when the Firebase Realtime Database updates the store will not get updated. :(
The listener only triggers inside the noteAPI and therefore never gets dispatched to the store.
What would the recommended best practice be for fetching and updating data?
Should I only fetch initial data with ".once()" and then write separate code to handle listeners with ".once()"? In that case where would that code "live"?
In other words - how can I structure my code so that the Firebase Realtime Database listener updates the Redux store automatically?
I have read the documentation on read and writes. https://firebase.google.com/docs/database/web/read-and-write