Infinite loop (react firebase firestore reactfire) - javascript

I have the following react component that creates a new document ref and then subscribes to it with the useFirestoreDocData hook.
This hook for some reason triggers an infinite rerender loop in the component.
Can anyone see what might cause the issue?
import * as React from 'react';
import { useFirestore, useFirestoreDocData, useUser } from 'reactfire';
import 'firebase/firestore';
import 'firebase/auth';
import { useEffect, useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
interface ICreateGameProps {
}
interface IGameLobbyDoc {
playerOne: string;
playerTwo: string | null;
}
const CreateGame: React.FunctionComponent<ICreateGameProps> = (props) => {
const user = useUser();
const gameLobbyDocRef = useFirestore()
.collection('GameLobbies')
.doc()
//This for some reason triggers an infinite loop
const { status, data } = useFirestoreDocData<IGameLobbyDoc>(gameLobbyDocRef);
const [newGameId, setNewGameId] = useState('')
const history = useHistory();
useEffect(() => {
async function createGameLobby() {
const gl: IGameLobbyDoc = {
playerOne: user.data.uid,
playerTwo:null
}
if (user.data.uid) {
const glRef = await gameLobbyDocRef.set(gl)
setNewGameId(gameLobbyDocRef.id)
}
}
createGameLobby()
return () => {
gameLobbyDocRef.delete();
}
}, [])
return <>
<h2>Gameid : {newGameId}</h2>
<p>Waiting for second player to join...</p>
<Link to="/">Go Back</Link>
</>
};
export default CreateGame;

The problem is when you're calling doc() without arguments:
firestore creates new document ref each time with new auto-generated id.
you pass this reference to the useFirestoreDocData that is responsible for creating and observing this document.
useFirestoreDocData makes request to the server to inform about new draft document.
server responds to this request with ok-ish response (no id conflicts, db is accessible, etc...).
created observer updates status of the created document
that triggers rerender (since the document data has updated).
on new rerender .doc() creates new document ref
gives it to the useFirestoreDocData and...
I believe you've got the idea.
To break out of this unfortunate loop we should ensure the .doc() call happens only once on the first render and the ref created by the it doesn't change on each rerender. That's exactly what useRef is for:
...
const gameLobbyDocRef = React.useRef(useFirestore()
.collection('GameLobbies')
.doc())
const { status, data } = useFirestoreDocData<IGameLobbyDoc>(gameLobbyDocRef.current);
...

Related

React Native: Error: Invalid hook call. Hooks can only be called inside of the body of a function component

in my react native app I have started playing with custom hooks, I created a hook to retrive user coordinates, however when my useGeolocation hook is called (inside handleUpdateLocation method) I always get the following warning:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
Actually, all my components are functional components...
Here is my component:
//import * as React from 'react';
import { View, Text, StyleSheet,TouchableOpacity, Platform,PermissionsAndroid, BackHandler, ScrollView, TextInput, ActivityIndicator ,SafeAreaView} from 'react-native';
import React, { useState, useEffect } from 'react'
import { observer } from 'mobx-react'
import useGeolocation from '#hooks/useGeolocation.js';
export default OrderCard14 = observer((props) =>
{
const handleUpdateLocation = async (gender) =>
{
const { coordinates, city } = useGeolocation(true);
};
return(
<View style={{ flex:1,backgroundColor:'white',alignItems:'center',padding:0 }}>
</View>
);
});
And my simplified hook(removed some functions):
//import AsyncStorage from '#react-native-async-storage/async-storage';
import { AsyncStorage ,Platform, Alert, PermissionsAndroid } from 'react-native';
import _ from 'lodash';
import env from '#env/vars';
import http from '#env/axiosin';
import Geolocation from 'react-native-geolocation-service';
import { useStore } from '#hooks/use-store';
const useGeolocation = (getCityName = false) => {
const root = useStore();
const [coordinates, setCoordinates] = useState(false);
const [city, setCity] = useState(false);
const requestLocationPermissions = async () =>
{
console.log('getting new coordinates');
if (Platform.OS === 'ios')
{
const auth = await Geolocation.requestAuthorization("whenInUse");
if(auth === "granted")
{
//root.mapStore.setLocationEnabled(true);
this.locationEnabled = true;
let coords = await getMe(getCityName);
return coords;
/*
const todayId = moment().isoWeekday();
if(todayId != root.userStore.user.lastLoginDayId)
{
getMe();
}
*/
}
else
{
//Alert.alert('PLEASE ENABLE LOCATION');
root.mapStore.setLocationEnabled(false);
//this.locationEnabled = false;
}
}
if (Platform.OS === 'android')
{
let granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);
if (granted === PermissionsAndroid.RESULTS.GRANTED)
{
// do something if granted...
//this.loactionEnabled = true;
root.mapStore.setLocationEnabled(true);
let coords = await getMe();
return coords;
/*
const todayId = moment().isoWeekday();
if(todayId != root.userStore.user.lastLoginDayId)
{
getMe();
}
*/
}
else
{
//Alert.alert('KULO');
root.mapStore.setLocationEnabled(false);
//this.locationEnabled = false;
}
}
}
useEffect(() => {
requestLocationPermissions();
}, []);
console.log('returning new coordinates with hook: '+coordinates);
return { coordinates, city };
};
export default useGeolocation;
What's exactly the problem?
Invalid hook call. Hooks can only be called inside of the body of a function component.
I think they should add to the list of possible/common reasons that:
You might be conditionally invoking the hook call on any given render, or not invoking it at all.
Hooks not only need to be inside the body of a function component, they need to be at the level of the body of the function component. Always called on every render, always in the same order. You have one that's inside a function:
const handleUpdateLocation = async (gender) =>
{
const { coordinates, city } = useGeolocation(true);
};
Which is invalid. Instead, move the hook call to the component level. You can still use the resulting values inside the function:
const { coordinates, city } = useGeolocation(true);
const handleUpdateLocation = async (gender) =>
{
// here you can still use coordinates and city
};
In general, it's a good rule of thumb to invoke all of the hooks that you'll need as the very first thing you do in any given component.

Why is a function inside a component called an infinite number of times?

I have React component :
import { Hotels } from "./Hotels";
import WelcomePage from "./WelcomePage";
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.6.0/firebase-app.js";
import {
getFirestore,
collection,
getDocs,
addDoc,
} from "https://www.gstatic.com/firebasejs/9.6.0/firebase-firestore.js";
import {
getAuth,
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
onAuthStateChanged,
signOut,
} from "https://www.gstatic.com/firebasejs/9.6.0/firebase-auth.js";
import { firebaseConfig, app, db, auth } from "../firebaseConfig";
import { useState } from "react";
function MainPage() {
const [hotels, setHotels] = useState([]);
const [authentication, setAuthentication] = useState(false);
async function fetchHotels() {
const _hotels = [];
const querySnapshot = await getDocs(collection(db, "reviews"));
querySnapshot.forEach((doc) => {
_hotels.push(doc.data());
});
console.log("fetched!");
setHotels(_hotels);
}
function isAuthenticated() {
onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
const uid = user.uid;
setAuthentication(true);
} else {
// User is signed out
setAuthentication(false);
}
});
}
isAuthenticated();
fetchHotels();
return (
<main className="content">
<Hotels hotels={hotels} />
</main>
);
}
export default MainPage;
After the application starts, the fetchHotels function starts to be called endlessly (this is evidenced by console.log("fetched!") ).
Under the same conditions, in other components, other functions are called adequately.
You're calling fetchHotels at the top level of your function component, so it's called on every render. In fetchHotels, you eventually call setHotels with a new array, which causes a re-render (since the new array by definition is different from the current one). So when that render happens, it calls fetchHotels again, which eventually calls setHotels again, which causes...
You need to only call fetchHotels at appropriate times. For instance, it looks like you only need to do that when the component first mounts, in which case you'd do it inside a useEffect callback with an empty dependency array (so that it is only run on component mount). And since nothing else calls it, you can just do the fetch right there in the callback:
useEffect(() => {
let cancelled = false;
(async () => {
try {
const snapshot = await getDocs(collection(db, "reviews"));
if (!cancelled) {
const hotels = snapshot.map(doc => doc.data());
setHotels(hotels);
console.log("fetched!");
}
} catch(error) {
// ...handle/report error
}
})();
return () => {
// Flag so we don't try to set state when the component has been
// unmounted. Ideally, if `getDocs` has some way of being cancelled
// (like `AbortController`/`AbortSignal`), do that instead; using
// a flag like this doesn't proactively stop the process.
cancelled = true;
};
}, []);
Note I added error handling; don't let an async function throw errors if nothing is going to handle them. Also, I used map to more idiomatically build an array (hotels) from another array (snapshot).
(You have the same basic problem with isAuthenticated. The only reason it doesn't cause an infinite loop is that it calls setAuthentication with a boolean value, so the second time it does that, it's setting the same value that was already there, which doesn't cause a re-render.)
You can't invoke functions in a component like that. What you need to do is invoke it in useEffect and (optional) give it a parameter that triggers the useEffect function.

How to get object out of React component

I am making an weather app with API, I am successfully receiving the data with API in the function but I am not able to take it out of the function
here is the code
import React, {useState} from 'react'
import {Text, View} from 'react-native'
const axios = require('axios');
let HomeScreen =() => {
let key = "XXXX"
axios.get(`https://api.weatherapi.com/v1/current.json?key=${key}&q=London&aqi=no`)
.then(function (response) {
// handle success
console.log(response)
})
return(
<Text>This is {response}</Text>
)
}
export default HomeScreen
If you want to simply return data from the API use a normal JS function, not a React component.
function getData() {
return axios(`https://api.weatherapi.com/v1/current.json?key=${key}&q=London&aqi=no`)
}
It will return a promise which you can then use somewhere else.
async main() {
const data = await getData();
}
If you want your component to render data retrieved from the API you need to do things differently.
Use useEffect to run once when the component is mounted, and use useState to store the data once it's been retrieved. The state will then inform the JSX how to render.
Note that the response from the API is an object with location and current properties. You can't just add that to the JSX because React won't accept it. So, depending on what value you need from the data, you need to target it specifically.
Here's an example that returns the text value from the condition object of the current object: "It is Sunny".
const { useEffect, useState } = React;
function Example() {
// Initialise your state with an empty object
const [data, setData] = useState({});
// Call useEffect with an empty dependency array
// so that only runs once when the component is mounted
useEffect(() => {
// Retrieve the data and set the state with it
async function getData() {
const key = 'XXX';
const data = await axios(`https://api.weatherapi.com/v1/current.json?key=${key}&q=London&aqi=no`)
setData(data.data);
}
getData();
}, []);
// If there is no current property provide a message
if (!data.current) return <div>No data</div>;
// Otherwise return the current condition text
return (
<p>It is {data.current.condition.text}</p>
);
}

React Hooks and ActionCable

Trying to get along with React new Hooks and ActionCable, but stuck with the problem that I can't get the right data in Rails when trying to send state.
I've tried to use send() method immediately after doing setState() and send my updated data, but for some reason, the data which received on the Rails part is old.
For example, if I put "Example" to the input I'll see "{"data"=>"Exampl"} on the Rails side. I suppose the data update the state later than my request goes.
If I send() value from e.target.value everything works fine
Therefore I've tried to use new useEffect() hook and send data there. But I get only data when rendering the page. Afterward, I don't get anything and sometimes get error RuntimeError - Unable to find subscription with an identifier. Seems like effect hook sends data too early or something.
I'm pretty new to Hooks and WebSockets. Would love to get any help here. I can share Rails code, but there is only a receiver and nothing else.
First exmaple:
import React, { useState, useEffect } from "react"
import ActionCable from 'actioncable'
function Component(props) {
const [data, setData] = useState("");
const cable = ActionCable.createConsumer('ws://localhost:3000/cable');
const sub = cable.subscriptions.create('DataChannel');
const handleChange = (e) => {
setData(e.target.value)
sub.send({ data });
}
return (
<input value={data} onChange={handleChange}/>
)
}
Tried to useEffect and move send() there:
useEffect(() => {
sub.send({ data });
}, [data]);
I'd love to find a way to correctly use React and ActionCable. And use hooks if it's possible.
I was trying an approach similar to Oleg's but I could not setChannel inside the action cable create subscription callback. I had to setChannel outside of the callback but within the useEffect hook. Below is the solution that worked for me.
create consumer in index.js and provide the consumer through Context to App.
index.js
import React, { createContext } from 'react'
import actionCable from 'actioncable'
... omitted other imports
const CableApp = {}
CableApp.cable = actionCable.createConsumer('ws://localhost:3000/cable')
export const ActionCableContext = createContext()
ReactDOM.render(
<Router>
... omitted other providers
<ActionCableContext.Provider value={CableApp.cable}>
<App />
</ActionCableContext.Provider>
</Router>,
document.getElementById('root')
)
Use the cable context in your child component and create subscription in useEffect hooks; unsubscribe in clean up
import React, { useState, useEffect, useContext } from 'react'
import { useParams } from 'react-router-dom'
... omitted code
const [channel, setChannel] = useState(null)
const { id } = useParams()
const cable = useContext(ActionCableContext)
useEffect(() => {
const channel = cable.subscriptions.create(
{
channel: 'MessagesChannel',
id: id,
},
{
received: (data) => {
receiveMessage(data)
},
}
)
setChannel(channel)
return () => {
channel.unsubscribe()
}
}, [id])
const sendMessage = (content) => {
channel.send(content)
}
You can register your cable at root component like that:
import actionCable from 'actioncable';
(function() {
window.CableApp || (window.CableApp = {});
CableApp.cable = actionCable.createConsumer('ws://localhost:3000/cable')
}).call(this);`
so it will be available as global variable;
and then in any component where you want to create channel and send data:
const [channel, setChannel] = useState(null);
useEffect(() => {
CableApp.cable.subscriptions.create(
{
channel: 'YourChannelName',
},
{
initialized() {
setChannel(this)
},
},
);
}, []);
return <button onClick={() => channel.send(some_data)} >Send counter</button>
Your problem is here:
const handleChange = (e) => {
setData(e.target.value)
sub.send({ data });
}
setData is like setState in that the state is only updated after the render i.e. after the function has exited. You are sending the current data not the new data. Try this:
const handleChange = (e) => {
const newData = e.target.value;
setData(newData)
sub.send({ data: newData });
}

ComponentDidMount returns undefined after initial mount

My component correctly mounts after reading from firebase one time but it fails on the second attempt and says that it cant get the value because "snap" is undefined. Im new to react but I assume something is missing a binding?
import React, { Component } from "react";
import "./solar.css";
import firebase from "firebase";
import { config } from "../config";
let app = firebase.initializeApp(config);
let database = app
.database()
.ref()
.child("values")
.child("Voltage");
class Solar extends Component {
state = {
voltage: 0
};
componentDidMount() {
this.interval = setInterval(
database.on("value", snap => {
this.setState({
voltage: snap.val()
});
console.log(snap.val());
}),
5000
);
}
componentWillUnmount() {
clearInterval(this.interval);
}
Your setInterval function is calling the event handler that you're passing for the "value" event.
This happens because the Database#on method returns the callback that you pass to on (to make de-registration of the event later during your cleanup phase).
So, when setInterval calls your callback, it won't be able to pass in a snap parameter seeing the callback is invoked by it (rather that the database instance) - that explains why snap is undefined.
Also - I'm not sure what purpose setInterval serves here. It seems you just want the component to update (re-render) when a value change is detected in your database. In that case, the call to setState() as you have is sufficient for this.
Consider revising your code as follows:
import React, {
Component
} from "react";
import "./solar.css";
import firebase from "firebase";
import {
config
} from "../config";
let app = firebase.initializeApp(config);
let database = app
.database()
.ref()
.child("values")
.child("Voltage");
class Solar extends Component {
state = {
voltage: 0
};
componentDidMount() {
// Store reference to the "on value" callback for deregistering
// the event when the Solar component unmounts
this.valueChangeCallback = database.on("value", snap => {
this.setState({
voltage: snap.val()
});
console.log(snap.val());
})
}
componentWillUnmount() {
// If a valueChangeCallback exists from former mount then deregister
// this callback from you database instance
if(this.valueChangeCallback) {
database.on("value", this.valueChangeCallback);
this.deregisterCallback = '';
}
}
}

Categories