ComponentDidMount returns undefined after initial mount - javascript

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 = '';
}
}
}

Related

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.

Access data from onmessage function from a websocket in Reactjs

I have established a websocket connect from my server to my client machine. I have parsed the data into an object and would like to access the data for representation on my front end.
import './App.css';
import { w3cwebsocket as W3CWebSocket } from "websocket";
import { Component } from 'react';
const client = new W3CWebSocket('ws://xyz:9080/user');
class App extends Component {
componentDidMount() {
client.open = () => {
console.log("Connected");
};
client.onmessage = (e) => {
const object = JSON.parse(e.data);
console.log(object.Snapshot);
}
client.onclose = () => {
console.log("Closed...");
}
}
render() {
return (<div className="App">
<h2>{ object }</h2>
</div>
);
}
}
export default App;
I want to access my object variable from the on message function and use it as a variable in my render function. How do I approach this?
You need to add local state to your class. State is a fairly foundational part of react and how it is able to reactively rerender components, so it sounds like you need to spend some time reading the docs to familiarize yourself with the basics.
That said, I'll provide an updated version of your code for demonstration purposes. Note that you used client.open when you meant client.onopen, so I've made that correction below:
import "./App.css";
import { w3cwebsocket as W3CWebSocket } from "websocket";
import { Component } from "react";
const client = new W3CWebSocket("ws://xyz:9080/user");
class App extends Component {
constructor(props) {
super(props);
this.state = { object: "" };
}
componentDidMount() {
client.onopen = () => {
console.log("Connected");
};
client.onmessage = (e) => {
const object = JSON.parse(e.data);
this.setState({ object: object });
console.log(object.Snapshot);
};
client.onclose = () => {
console.log("Closed...");
};
}
render() {
return (
<div className="App">
<h2>{this.state.object}</h2>
</div>
);
}
}
export default App;
Also, since it seems that you're probably just starting out with react, I would strongly recommend that instead of the old-style class-based components, you use learn to use hooks and functional components, which is just an overall much cleaner and easier to reason about way to write react code. We could rewrite your code as follows using the useState and useEffect hooks in an App function:
import "./App.css";
import { w3cwebsocket as W3CWebSocket } from "websocket";
import { useEffect, useState } from "react";
export default function App() {
const [object, setObject] = useState("");
useEffect(() => {
const client = new W3CWebSocket("ws://xyz:9080/user");
client.onopen = () => {
console.log("Connected");
};
client.onmessage = (e) => {
const newObj = JSON.parse(e.data);
setObject(newObj);
console.log(newObj.Snapshot);
};
client.onclose = () => {
console.log("Closed...");
};
return () => client.OPEN && client.close();
}, []);
return (
<div className="App">
<h2>{object}</h2>
</div>
);
}
Note per the docs that useEffect with an empty dependency array is more or less equivalent to componentDidMount. Note also that even though client is defined in a local scope, it won't be garbage-collected, because it is referenced in the cleanup closure (the return value of the arrow function passed to useEffect).
Finally, note that I haven't used the websocket package before, so I don't know if your usage is correct or optimal. This answer is about how to manage state in react, not how to use websocket in a react application.

Infinite loop (react firebase firestore reactfire)

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);
...

ReactJS Working with API calls - data changes everytime I use a component

For my project I'm using data from https://api.randomuser.me/ which basically returns a random person every time you fetch the data.
I've created a component (for the purpose of the question I've simplified it) that returns a first name of person fetched by the API. However, if I use the component twice I get two different names all the time. How can I reuse the component and customise it (using props etc.) while using the same dataset which then refreshes every 10 seconds as in the code below?
import React,{Component} from "react";
import { makeStyles } from '#material-ui/styles';
import Budgetdata from './Budgetdata.js'
import Caption from './Caption.js'
export default class Apicall extends Component {
intervalID;
constructor(props) {
super(props);
this.state = {
loading:true,
person:[],
random: 0,
name:"name"};
}
async getdata () {
const url = "https://api.randomuser.me/";
const response = await fetch(url);
const data = await response.json();
var value = Math.round(Math.random())
this.setState({ person: data.results[0],loading:false,random: value,name:"name"})
}
async componentDidMount() {
this.getdata()
this.intervalID = setInterval(this.getdata.bind(this), 10000);
}
async componentWillUnmount() {
clearInterval(this.intervalID);
}
render = props => {
if (this.state.loading || !this.state.person) {
return (<div>loading..</div>)
}
else
{
return(
<div>{this.state.person.name.first}</div>
)
}
}
}
Based on
https://www.valentinog.com/blog/await-react/
await is not well supported at the front end side. I will suggest you to use Axios libraries.
On the other hand, if you need to use a react component twice you could need to use the react component key field as told at:
https://blog.cloudboost.io/key-concept-and-its-necessities-in-react-component-885c18084e59

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 });
}

Categories