Update value of variable from Firebase query? - reactjs

I currently am trying to pull the value of an item from a firebase using once, and use this value to populate the var itemsList. However, although itemsList is correctly populated within the once call, it is logging as undefined outside the call. I think this may have something to do with the asynchronous nature of Firebase, but I'm not sure how to remedy this problem. Any suggestions?
submitAnswer: function() {
var userRef = usersRef.child(key);
//get items_list for user
var itemsList;
userRef.once('value', (snap) => {
itemsList = snap.val().items_list;
console.log('items list populated here' + itemsList);
});
console.log("items list not populated here" + itemsList);
},

You're correct about the Firebase call, it's asynchronous and the code isn't going to wait for it to complete before logging the unpopulated itemsList.
If you're only looking to do something simple with the data, just be sure to check that it exists before performing any action with it (and handle it like you would any async data).
if(itemsList){
console.log('My items have arrived! ' + itemsList);
}
If that data is going to be propagated further down your app it is usually suggested to make a call to setState() with your response data from Firebase to trigger a re-render of your components with the new data you just fetched.
So something along the lines of:
userRef.once("value", (snap) => {
itemsList = snap.val().items_list;
this.setState({
items: itemsList;
});
}.bind(this));

Related

Issues accessing react state in firestore onSnapshot listener

I want to wait to apply state updates from the back-end if a certain animation is currently running. This animation could run multiple times depending on the game scenario. I'm using react-native with hooks and firestore.
My plan was to make an array that would store objects of the incoming snapshot and the function which would use that data to update the state. When the animation ended it would set that the animation was running to false and remove the first item of the array. I'd also write a useEffect, which would remove the first item from the array if the length of the array had changed.
I was going to implement this function by checking whether this animation is running or whether there's an item in the array of future updates when the latest snapshot arrives. If that condition was true I'd add the snapshot and the update function to my array, otherwise I'd apply the state update immediately. I need to access that piece of state in all 3 of my firestore listeners.
However, in onSnapshot if I try to access my state it'll give me the initial state from when the function rendered. The one exception is I can access the state if I use the function to set the state, in this case setPlayerIsBetting and access the previous state through the function passed in as a callback to setPlayerIsBetting.
I can think of a few possible solutions, but all of them feel hacky besides the first one, which I'm having trouble implementing.
Would I get the future state updates if I modify the useEffect for the snapshots to not just run when the component is mounted? I briefly tried this, but it seems to be breaking the snapshots. Would anyone know how to implement this?
access the state through calling setPlayerIsBetting in all 3 listeners and just set setPlayerIsBetting to the previous state 99% of the time when its not supposed to be updated. Would it even re-render if nothing is actually changed? Could this cause any other problems?
Throughout the component lifecycle add snapshots and the update functions to the queue instead of just when the animation is running. This might not be optimal for performance right? I wouldn't have needed to worry about it for my initial plan to make a few state updates after an animation runs since i needed to take time to wait for the animation anyway.
I could add the state I need everywhere on the back-end so it would come in with the snapshot.
Some sort of method that removes and then adds the listeners. This feels like a bad idea.
Could redux or some sort of state management tool solve this problem? It would be a lot of work to implement it for this one issue, but maybe my apps at the point where it'd be useful anyway?
Here's my relevant code:
const Game = ({ route }) => {
const [playerIsBetting, setPlayerIsBetting] = useState({
isBetting: false,
display: false,
step: Infinity,
minimumValue: -1000000,
maximumValue: -5000,
});
const [updatesAfterAnimations, setUpdatesAfterAnimations] = useState([]);
// updatesAfterAnimations is currently always empty because I can't access the updated playerIsBetting state easily
const chipsAnimationRunningOrItemsInQueue = (snapshot, updateFunction) => {
console.log(
"in chipsAnimationRunningOrItemsInQueue playerIsBetting is: ",
playerIsBetting
); // always logs the initial state since its called from the snapshots.
// So it doesn't know when runChipsAnimation is added to the state and becomes true.
// So playerIsBetting.runChipsAnimation is undefined
const addToQueue =
playerIsBetting.runChipsAnimation || updatesAfterAnimations.length;
if (addToQueue) {
setUpdatesAfterAnimations((prevState) => {
const nextState = cloneDeep(prevState);
nextState.push({ snapshot, updateFunction });
return nextState;
});
console.log("chipsAnimationRunningOrItemsInQueue returns true!");
return true;
}
console.log("chipsAnimationRunningOrItemsInQueue returns false!");
return false;
};
// listener 1
useEffect(() => {
const db = firebase.firestore();
const tableId = route.params.tableId;
const unsubscribeFromPlayerCards = db
.collection("tables")
.doc(tableId)
.collection("players")
.doc(player.uniqueId)
.collection("playerCards")
.doc(player.uniqueId)
.onSnapshot(
function (cardsSnapshot) {
if (!chipsAnimationRunningOrItemsInQueue(cardsSnapshot, updatePlayerCards)) {
updatePlayerCards(cardsSnapshot);
}
},
function (err) {
// console.log('error is: ', err);
}
);
return unsubscribeFromPlayerCards;
}, []);
};
// listener 2
useEffect(() => {
const tableId = route.params.tableId;
const db = firebase.firestore();
const unsubscribeFromPlayers = db
.collection("tables")
.doc(tableId)
.collection("players")
.onSnapshot(
function (playersSnapshot) {
console.log("in playerSnapshot playerIsBetting is: ", playerIsBetting); // also logs the initial state
console.log("in playerSnapshot playerIsBetting.runChipsAnimation is: "playerIsBetting.runChipsAnimation); // logs undefined
if (!chipsAnimationRunningOrItemsInQueue(playersSnapshot, updatePlayers)) {
updatePlayers(playersSnapshot);
}
},
(err) => {
console.log("error is: ", err);
}
);
return unsubscribeFromPlayers;
}, []);
// listener 3
useEffect(() => {
const db = firebase.firestore();
const tableId = route.params.tableId;
// console.log('tableId is: ', tableId);
const unsubscribeFromTable = db
.collection("tables")
.doc(tableId)
.onSnapshot(
(tableSnapshot) => {
if (!chipsAnimationRunningOrItemsInQueue(tableSnapshot, updateTable)) {
updateTable(tableSnapshot);
}
},
(err) => {
throw new err();
}
);
return unsubscribeFromTable;
}, []);
I ended up not going with any of the solutions I proposed.
I realized that I could access the up to date state by using a ref. How to do it is explained here: (https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559) And this is the relevant code sample from that post: (https://codesandbox.io/s/event-handler-use-ref-4hvxt?from-embed)
Solution #1 could've worked, but it would be difficult because I'd have to work around the cleanup function running when the animation state changes. (Why is the cleanup function from `useEffect` called on every render?)
I could work around this by having the cleanup function not call the function to unsubscribe from the listener and store the unsubscribe functions in state and put them all in a useEffect after the component mounts with a 2nd parameter that confirmed all 3 unsubscribe functions had been added to state.
But if a user went offline before those functions were in state I think there could be memory leaks.
I would go with solution #1: In the UseEffect() hooks you could put a boolean flag in so the snapshot listener is only set once per hook. Then put the animation state property in the useEffect dependency array so that each useEffect hook is triggered when the animation state changes and you can then run whatever logic you want from that condition.

How to use the rerenderEvents() method on a pre-rendered Calendar

UPDATED 3-26-2019
I am trying to give users the ability to add events to the Calendar and for those events to appear upon input. I am using a React.js front end and can get the events to render in a new calendar but it does not delete the old one (I just end up with 10+ calenders that render on top of each other).
I've tried using the .destroy() method and then re-rendering the calendar but the method doesn't seem to be available/functioning so I figured I would try rerenderEvents() and keep the same calendar object but that function doesn't seem to be accessible either. Would love some assistance in solving this from anyone who is familiar with FullCalendar v.4
Once the user inputs the data it's collected into a data object and append it into the state but if I just mirror the above, it renders a whole new calendar on top of the old one.
It's like I cannot get the Calendar object once rendered to make those method calls, but am having trouble capturing it in a separate function. All the Docs say is call the .destroy() or call the .rerenderEvents() and not what to call them on.
//This successfully Renders
componentOnMount() {
var calendarNew = document.getElementById('calendar');
let newCalendar = new Calendar(calendarNew, {
plugins: [ dayGridPlugin, timeGridPlugin, listPlugin ],
events: this.state.events
});
console.log(newCalendar)
await this.setState({ calendarObj: newCalendar })
// let myNewEvents = newCalendar.getEvents()
// console.log(myNewEvents)
// let stateEvents = this.state.calendarObj.getEvents()
// console.log(stateEvents)
this.state.calendarObj.render();
}
I've tried the following to mitigate the double render...
async handleNewEvent() {
// code that creates an object and sets the state to the new event array//
var newEventArr = existingEvents.concat(newEvent)
console.log(newEventArr)
await this.setState({ events: newEventArr })
await this.setState({ currentIndex: (this.state.currentIndex + 1) })
this.props.change(this.state.events, this.state.currentIndex)
//Contains the new event array in the console
console.log(this.state.events)
this.state.calendarObj.rerenderEvents();
}
But this does nothing. I cannot even render() from the handleNewEvent() as it appears as if the state is unable to hold functions in it and I cannot find a way to pass the function from the ComponentDidMount() to the handleNewEvent(). Im at a loss...

React Flux - Return value from flux dispatcher / store

I am using the flux-pattern and the flux dispatcher. I need to return a value from 'TextStore' to an action after creating a new TextItem because I need to reference it in another store.
Here is a very simple version of what I want to do:
// stores.ts
var TextStore = {
add(){
// here I want to return a new ID
return createRandomID();
}
...
}
var ModuleStore = {
labelTextID; // refers to `id` on Text
...
}
// moduleactions.ts
...
import PageActions from './PageActions';
var ModuleActions = {
add: function (module) {
var textID = PageActions.add(); // here I need to get the ID of the newly create `Text`
module.labelTextID = textID;
Dispatcher.dispatch({
action: 'ADD_MODULE',
module: module
})
},
...
}
Now when I add a new Module via dispatching an action, I want to create a new Text as well and return its newly created ID from the store before.
The most obvious way would be to require the TextStore inside ModuleActions and call add() directly. Is that against the flux-pattern?
Is there any way to accomplish that, maybe with promises? Sending callbacks via the dispatcher to the store doesnt work, because I cannot dispatch while another dispatch is unfinished.
Would be great if you guys can help me!
Calling the Store's method directly is an anti-pattern for Flux. If you directly call TextStore.add() then you are not following the
Action -> Dispatcher -> Store --> View --> Action cycle.
In flux, the data should always generate in the action. This makes more sense when the process of generation of data is aync. In your case you were able to get around this because generation of data is not async. You are directly generating the data in the TextStore and then worrying about how to return that value.
In your case generate the id in the action as you would have done if it was an async backend event, then pass that id down to the TextStore via dispatcher and after that also pass the same id to the ModuleStore via the dispatcher.
var ModuleActions = {
add: function (module) {
var textID = new Date().getTime(); // this is the CHANGE i added
PageActions.add(textID);
module.labelTextID = textID;
Dispatcher.dispatch({
action: 'ADD_MODULE',
module: module
})
}
}
You could also break this down into further smaller, more specific actions. I kept it as-is so I could highlight the one line you should change.
Is there any way to accomplish that, maybe with promises? Sending callbacks via the dispatcher to the store doesnt work, because I cannot dispatch while another dispatch is unfinished.
You can have your async call PageActions.add(); before you dispatch the ModuleActions.add and pass the returned value as a parameter to ModuleActions.add

How to avoid 'children with same key' when replacing state in ReactJs component

I have created a React Component that renders a set of sub-elements given an array of ids. The array of ids is kept in the state of the parent component, and then I run some ajax calls based on the ids to fetch data to render. The fetched data is stored in a separate data array in the state. The rendered components use the id as key.
The ids can change based on actions outside of the component, so I use setState on the component to replace the array. The updated id-state will probably contain some of the same ids as the in the original array. At the same time I empty the 'data array' so that everything will be rendered again.
When I do this I sometimes get the key-warning:
Warning: flattenChildren(...): Encountered two children with the same
key. Child keys must be unique; when two children share a key, only
the first child will be used.
The new array does not contain any duplicates. So why does it happen, and what can I do to avoid this?
Edit: Added some code by request. Note: I am using the Infinite Scroll module. Could this be causing it?
Initial state:
getInitialState: function() {
return {
hasMore: true,
num: 0,
movieIds: this.props.movieIds,
movies: []
};
},
Render function:
render: function() {
var InfiniteScroll = React.addons.InfiniteScroll;
return (
<InfiniteScroll
pageStart={0}
loadMore={this.loadMore}
threshold='20'
hasMore={this.state.hasMore}>
<ul className="movieList">
{this.state.movies}
</ul>
</InfiniteScroll>
);
}
Simplified load more:
comp = this;
$.ajax( {
url: url,
contentType: "json",
success: function (data) {
var m = createMovieLi(data);
var updatedMovies = comp.state.movies;
updatedMovies[num] = m;
comp.setState({movies: updatedMovies});
}
});
And finally when updating outside the component:
movieBox.setState({
hasMore: true,
num: 0,
movieIds: filteredIds,
movies: []
});
I figured out my mistake, and it had nothing to do with React per se. It was a classic case of missing javascript closure inside a loop.
Because of the possibility of duplicates I stored each ajax response in window.localStorage, on the movieId. Or so I thought.
In React Inifinite Scroll each item in your list is drawn sequentially with a call to the loadMore-function. Inside this function I did my ajax call, and stored the result in the browser cache. The code looked something like this:
var cachedValue = window.localStorage.getItem(String(movieId));
var cachedData = cachedValue ? JSON.parse(cachedValue) : cachedValue;
if (cachedData != null) {
comp.drawNextMovie(cachedData);
} else {
$.ajax( {
type: "GET",
url: this.state.movieUrl + movieId,
contentType: "json",
success: function (movieData) {
window.localStorage.setItem(String(movieId), JSON.stringify(movieData));
comp.drawNextMovie(movieData);
}
});
}
Can you spot the mistake? When the ajax-call returns, movieId is no longer what is was. So I end up storing the data by the wrong id, and get some strange React warnings in return. Because this was done inside the loadMore function called by the InfiniteScroll-module, I was not aware that this function was not properly scoped.
I fixed it by adding a Immediately-invoked function expression.
I wouldn't use the ID from a back-end as key property in React. If you do, you're relying on some logic that's a bit far away from your component to make sure that your keys are unique. If they keys are not unique, you can break react's rendering so this is quite important.
This is why you should, in my opinion, just stick to using the index within a for loop or similar to set key properties. That way you know they can never be non-unique, and it's the simplest way of doing exactly that.
Without knowing exactly how your IDs work it's impossible to say what's causing the non-unique clash here. However since key is just to allow React to correctly identify elements, and nothing else, it doesn't really make sense for it to be anything other than a simple count or index.
var children = [];
for (var i=0; i<yourArray.length; i++) {
children.push(
<div key={i}>{yourArray[i].someProp}</div>
);
}

Avoiding event chains with asynchronous data dependencies

The Facebook Flux dispatcher explicitly prohibits ActionCreators from dispatching other ActionCreators. This restriciton is probably a good idea since it prevents your application from creating event chains.
This however becomes an issue as soon as you have Stores containing data from asynchronous ActionCreators that depend on each other. If CategoryProductsStore depends on CategoryStore there doesn't seem to be a way to avoid event chains when without resorting to deferring the follow-up action.
Scenario 1:
A store containing a list of products in a category needs to know from which category ID it should fetch products from.
var CategoryProductActions = {
get: function(categoryId) {
Dispatcher.handleViewAction({
type: ActionTypes.LOAD_CATEGORY_PRODUCTS,
categoryId: categoryId
})
ProductAPIUtils
.getByCategoryId(categoryId)
.then(CategoryProductActions.getComplete)
},
getComplete: function(products) {
Dispatcher.handleServerAction({
type: ActionTypes.LOAD_CATEGORY_PRODUCTS_COMPLETE,
products: products
})
}
}
CategoryStore.dispatchToken = Dispatcher.register(function(payload) {
var action = payload.action
switch (action.type) {
case ActionTypes.LOAD_CATEGORIES_COMPLETE:
var category = action.categories[0]
// Attempt to asynchronously fetch products in the given category, this causes an invariant to be thrown.
CategoryProductActions.get(category.id)
...
Scenario 2:
Another scenario is when a child component is mounted as the result of a Store change and its componentWillMount/componentWillReceiveProps attempts to fetch data via an asynchronous ActionCreator:
var Categories = React.createClass({
componentWillMount() {
CategoryStore.addChangeListener(this.onStoreChange)
},
onStoreChange: function() {
this.setState({
category: CategoryStore.getCurrent()
})
},
render: function() {
var category = this.state.category
if (category) {
var products = <CategoryProducts categoryId={category.id} />
}
return (
<div>
{products}
</div>
)
}
})
var CategoryProducts = React.createClass({
componentWillMount: function() {
if (!CategoryProductStore.contains(this.props.categoryId)) {
// Attempt to asynchronously fetch products in the given category, this causes an invariant to be thrown.
CategoryProductActions.get(this.props.categoryId)
}
}
})
Are there ways to avoid this without resorting to defer?
Whenever you are retrieving the state of the application, you want to be retrieving that state directly from the Stores, with getter methods. Actions are objects that inform Stores. You could think of them as being like a request for a change in state. They should not return any data. They are not a mechanism by which you should be retrieving the application state, but rather merely changing it.
So in scenario 1, getCurrent(category.id) is something that should be defined on a Store.
In scenario 2, it sounds like you are running into an issue with the initialization of the Store's data. I usually handle this by (ideally) getting the data into the stores before rendering the root component. I do this in a bootstrapping module. Alternatively, if this absolutely needs to be async, you can create everything to work with a blank slate, and then re-render after the Stores respond to an INITIAL_LOAD action.
For scenario 1:
I would dispatch new the action from the view itself, so a new action -> dispatcher -> store -> view cycle will trigger.
I can imagine that your view needs to retrieve the category list and also it has to show, by default, the list of products of the first category.
So that view will react to changes con CategoryStore first. Once the category list is loaded, trigger the new Action to get the products of the first category.
Now, this is the tricky part. If you do that in the change listener of the view, you will get an invariant exception, so here you have to wait for the payload of the first action to be completely processed.
One way to solve this is to use timeout on the change listener of the view. Something similar to what is explained here:
https://groups.google.com/forum/#!topic/reactjs/1xR9esXX1X4 but instead of dispatching the action from the store, you would do it from the view.
function getCategoryProducts(id) {
setTimeout(() => {
if (!AppDispatcher.isDispatching()) {
CategoryProductActions.get(id);
} else {
getCategoryProducts(id);
}
}, 3);
}
I know, it is horrible, but at least you won't have stores chaining actions or domain logic leaking to action creators. With this approach, the actions are "requested" from the views that actually need them.
The other option, which I haven't tried honestly, is to listen for the DOM event once the component with the list of categories is populated. In that moment, you dispatch the new action which will trigger a new "Flux" chain. I actually think this one is neater, but as said, I haven't tried yet.

Resources