Infinite Loop when setting setState hook as class attribute in Effect hook - javascript

I have an Effect hook in a React component that sets up and initialises a class that I use to communicate with a backend server:
const SignalProvider = ({url, children}) => {
let [sigErr, setSigErr] = useState("")
let [token, setToken] = useContext(TokenContext)
let [signaller, setSignaller] = useState()
useEffect(() => {
if (!signaller) {
const s = new Signaller(url, (err) => setSigErr(err))
setSignaller(s)
}
if (token && signaller) {
signaller.setToken(token)
signaller.setSetTokenCallback(setToken) // Adding this line causes an infinite loop
signaller.connect()
}
}, [url, token, signaller, setToken, authError])
...
}
However, adding the line signaller.setSetTokenCallback(setToken) causes an infinite loop of re-rendering. Without this line it works as expected.
All setSetTokenCallback does is:
setTokenCallback(f) {
this.setTokenCallback = f
}
Which I don't think should matter.
Whats the best way to prevent the loop?

Have you tried using 'useCallback()' hook?
const initializeClass = useCallback(() => {
if (!signaller) {
const s = new Signaller(url, (err) => setSigErr(err))
setSignaller(s)
}
if (token && signaller) {
signaller.setToken(token)
signaller.setSetTokenCallback(setToken)
signaller.connect()
}, [url, token, signaller, setToken, authError])
useEffect(() => {
initializeClass()
}, [bla bla all dependencies needed])

I think your culprit is in these lines
signaller.setToken(token) // first possible culprit
signaller.setSetTokenCallback(setToken)
Since you are setting the token inside the useEffect and your useEffect depends on the token value for a re-evaluation again. If the token value is different than the previous one then it will make the useEffect to re-render.
[url, token, signaller, setToken, authError] // second possible culprit
Also, you should remove object or array type variables from the condition array [url, token, signaller, setToken, authError] or stringify them or find some way to compare, as objects and arrays can't be compared directly. They will always return false if directly compared. Thus, your useEffect will re-run.

Related

How to identify an element in a table whatever its place

Here is the new script with the find function which allows me to identify a single element of the array for sure but there is still a small problem. As you can see it's my const crypto which contains the data I want to display on the front end. However when I want to call crypto at the return level the const is not recognized.
Hello again,
I updated the script now it works I can display on the front end the data I want however I have the impression that the request to the api is executed several times when I would like there to be only one request
I put below a screen of the console.log of my script.
As you can see the data requested is displayed first as undefined then it is displayed several times, then I put blocked due to the too large number of requests made in little time
Thanks for your help
How do I make my requests :
import { useState, useEffect } from "react";
function App() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null)
useEffect(() => {
fetch("http://localhost:4500/coingeckotest")
.then(response => {
if (response.ok) {
return response.json()
}
throw response;
})
.then(data => {
setData(data)
})
.catch(error => {
console.error("Error fetching data: ", error)
setError(error)
})
.finally(() => {
setLoading(false)
})
}, [])
const uniswap = data&&data[0].tickers?.find(donne =>
donne.trade_url === 'https://app.uniswap.org/#/swap?
inputCurrency=0x2260fac5e5542a773aa44fbcfedf7c193bc2c599&
outputCurrency=ETH')
const uniprice = uniswap?.converted_last?.usd
const sushiswap = data&&data[1].tickers?.find(donne =>
donne.trade_url === 'https://app.sushi.com/swap?
inputCurrency=0x2260fac5e5542a773aa44fbcfedf7c193bc2c59
9&outputCurrency=ETH')
const sushiprice = sushiswap?.converted_last?.usd
const curvefinance = data&&data[2].tickers?.find(donne =>
donne.base === 'DAI')
const curveprice = curvefinance?.converted_last?.usd
const quickswap = data&&data[3].tickers?.find(donne =>
donne.trade_url === 'https://quickswap.exchange/#/swap?
inputCurrency=0x0d500b1d8e8ef31e21c99d1db9a6444d3adf127
0&outputCurrency=0xbbba073c31bf03b8acf7c28ef0738decf369
5683')
const quickprice = quickswap?.converted_last?.usd
console.log(uniprice)
console.log(sushiprice)
console.log(curveprice)
console.log(quickprice)
if (loading) return "Loading..."
if(error) return "Error !"
return (
<>
</>
)
}
export default App;
Thank you in advance for your answers
You can use the Array.find method to find out the first entry that matches a particular coin_id. The code for that would be:
const desiredCoinID = "something"; (determined by some user input)
const desiredCoinObject = data.find(coin => coin.coin_id === desiredCoinID);
const priceInUSD = desiredCoinObject?.tickers?.converted_last?.usd;
——-Updated Answer——-
Hi, this is my answer to your updated question. const crypto that you use is available only within the scope of the callback of the useEffect function, which is why you cannot see the results on your screen. There are two ways you can go about it:
First, you can declare crypto as a let outside the useEffect and then update it inside your useEffect. That way your crypto will have global scope. But there is a better way to do this, which is to use the useState hook.
You can declare a crypto, setCrypto using useState and then use the setter to update the value if crypto inside useEffect after the data fetching is over. Let me know if you need help writing code.

Infinite loop when calling a module that uses a hook

In Main.tsx, I have been using useApi(opts,payload), which is a hook I made that is subscribed to changes in payload. It will make an API call whenever that variable is changed.
When I use usApi() in Main.tsx (as I have commented out), everything works fine.
It is only when I moved this logic to another functional component (Auth.tsx), and try to call it within Main.tsx that I encounter a problem. The problem is that it seems to attempt to make an API call in an infinite loop, but never throws an error.
What is happening here?
Main.tsx
function Main():JSX.Element {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const opts = {
username: username,
password: password,
fail: ()=>null
}
const defaultPayload = {
path: 'notes/validateAuth/',
method: 'GET',
body: null,
callback: ()=>null
}
const [payload, setPayload] = useState<IPayload>(defaultPayload)
//useApi(opts, payload) <---- THIS WORKS
Auth() // <---- THIS DOES NOT
...
Auth.tsx
import React, {useState} from 'react';
import useApi, {IPayload} from './hooks/useApi';
import Modal from 'react-modal'
import './modal.css';
Modal.setAppElement('#root')
function Auth():JSX.Element{
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const opts = {
username: username,
password: password,
fail: ()=>null,
}
const defaultPayload = {
path:'notes/validateAuth/',
method: 'GET',
body: null,
callback: ()=>null
}
//const _payload = (props.payload===null)?defaultPayload:props.payload
// console.log(_payload)
useApi(opts, defaultPayload)
return(
<></>
)
}
export default Auth;
UseApi.tsx
In case this is needed to debug:
import {useState, useEffect, useCallback} from 'react'
import createPersistedState from 'use-persisted-state'
import {apiUrl} from '../config'
console.log("MODE: "+apiUrl)
export interface IProps {
username:string,
password:string,
fail: ()=>void
}
export interface IPayload {
path:string,
method:string,
body:{}|null,
callback: (result:any)=>any,
}
function useApi(props:IProps, payload:IPayload){
const [accessKey, setAccessKey] = useState('')
const useRefreshKeyState = createPersistedState('refreshKey')
const [refreshKey, setRefreshKey] = useRefreshKeyState('')
//const [refreshKey, setRefreshKey] = useState('')
const [refreshKeyIsValid, setRefreshKeyIsValid] = useState<null|boolean>(null)
// const apiUrl = 'http://127.0.0.1:8000/api/'
// const apiUrl = '/api/'
const [accessKeyIsValid, setAccessKeyIsValid] = useState<null|boolean>(null)
const validHttpCodes = [200, 201, 202, 203, 204]
const go = useCallback((accessKey)=>{
const {body, method, path} = payload
console.log('executing GO:'+accessKey)
if(method === 'logout'){
return logout(payload.callback)
}
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer '+accessKey,
},
... (body !== null) && { body: JSON.stringify(body) }
}
fetch(apiUrl+path,options)
.then(response=>{
if(!validHttpCodes.includes(response.status)){
setAccessKeyIsValid(false)
} else {
setAccessKeyIsValid(true)
response.json()
.then(response=>{
payload.callback(response)
})
}
})
},[payload])
function logout(callback:(response:null)=>void){
setRefreshKey('')
setAccessKey('')
setRefreshKeyIsValid(null)
setAccessKeyIsValid(null)
callback(null)
}
useEffect(()=>{
if(accessKeyIsValid===false){
console.log('access key is false')
// We tried to make a request, but our key is invalid.
// We need to use the refresh key
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json', },
body: JSON.stringify( {'refresh': refreshKey} ),
}
fetch(apiUrl+'token/refresh/', options)
.then(response=>{
if(!validHttpCodes.includes(response.status)){
setRefreshKeyIsValid(false)
// this needs to trigger a login event
} else {
response.json()
.then(response=>{
setRefreshKeyIsValid(true)
setAccessKey(response.access)
// setAccessKeyIsValid(true)
})
}
})
}
},[accessKeyIsValid])
useEffect(()=>{
console.log('responding to change in access key:'+accessKey)
go(accessKey)
},[accessKey,payload])
useEffect(()=>{
if(refreshKeyIsValid===false){
// even after trying to login, the RK is invalid
// We must straight up log in.
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: props.username,
password: props.password,
})
}
fetch(apiUrl+'token/', options)
.then(response=>{
console.log(response.status)
if(!validHttpCodes.includes(response.status)){
setAccessKeyIsValid(null)
setRefreshKeyIsValid(null)
props.fail()
console.log('total fail')
}
else {
console.log('login works')
response.json()
.then(response=>{
setAccessKey(response.access)
setRefreshKey(response.refresh)
// setRefreshKeyIsValid(true)
// setAccessKeyIsValid(true) // Commenting this out disables the loop
})
}
})
}
},[refreshKeyIsValid])
};
export default useApi
Note: I have reasons for needing to factor this logic out, which is not evident by my abridged code.
We've spoken a little bit on chat in order to find out a little bit more about what is going on here. I'll edit your question in a moment to take out the irrelevant code so that this is easier to understand for anyone stumbling on the question.
Your original code did the following:
Instantiated an object (defaultProps) within a functional component
Passed that object in as the default for a useState() call
Passed the state value into your hook, which was then used in the dependency array of a useEffect() hook
Your refactor changed it so you were directly passing the defaultProps reference into your hook without going through a useState() hook as before.
An important thing to understand about React components is that although their execution pattern is somewhat predictable, it is something you have no control over as a developer and so you should assume when writing your components that they may be re-rendered at any time.
Within a functional component, each re-render is a new function execution with a new scope. If you instantiate an object within this scope, that's a new reference variable pointing to a different area of memory each time
The problem with then using this within the dependency array of a useEffect() hook is that since this is a new variable on each render, it means that each time the component is re-rendered, the useEffect hook will see a different reference, and cause the effect callback to be executed again. This is not normally the intended behaviour.
const component = () => {
/*
* Each time this component
* renders, this object is created again
* in (probably) a different area of memory
*/
const obj = {
foo: 'bar'
}
/*
* Although 'obj' *appears* to be the same here since it is in fact
* a different reference, this console.log statement is going
* to execute every single time the component re-renders
*/
useEffect(() => {
console.log('running effect!')
}, [obj])
return <div></div>
}
So what is the solution?
The original code first passed the defaultPayload into a useState call. This was the right thing to do. The reason for this is because component state values do maintain the same reference in between renders and can therefore be safely used in a useEffect() dependency array.
const component = () => {
/*
* still a different reference on each render
*/
const obj = {
foo: 'bar'
}
const [objState, setObjState] = React.useState(obj);
/*
* Although `obj` is a difference reference on every render
* objState is *not*, so our `console.log` will now only execute
* when objState changes
*/
useEffect(() => {
console.log('running effect!')
}, [objState])
return <div></div>
}
Alternative (but slightly pointless) Solution
Note that you could also resolve the very specific issue experienced in this question by simply instantiating the object outside of the functional component scope (ie, in the module global scope). However, you will need to put it into a state variable as soon as you want to be able to change it (which the name defaultPayload rather suggests you will)
/*
* Definitions made within the module global scope
* are instantiated only once, when the module is imported
*/
const obj = {
foo: 'bar'
}
const component = () => {
/*
* useEffect hook will now only ever fire once
* since `obj` is never going to change
*/
useEffect(() => {
console.log('running effect!')
}, [obj])
return <div></div>
}

How can I bypass React's useEffect() "missing dependency" warning?

I am creating a chat app in React using socket.io. I don't need the following useEffect() to run more than once, and what this warning message is asking me to resolve would conflict with my intention of setting the socket message handler only once:
React Hook useEffect has a missing dependency: 'messages'. Either include it or remove the dependency array. You can also do a functional update 'handleMessages(m => ...)' if you only need 'messages' in the 'handleMessages' call.eslintreact-hooks/exhaustive-deps
export default function Group(props) {
const [_broadcastOptions, handleBroadcastOptions] = useState(false);
const [messages, handleMessages] = useState([]);
const textValue = useRef('');
const bottomScrollRef = useRef();
const socket = useRef();
useEffect(()=>{
console.log('bruh');
socket.current = io('http://0.0.0.0:3001');
socket.current.on('connect', ()=> {
console.log('socket connection status: ' + socket.current.connected);
socket.current.emit('enter-room', getRoomId(window.location.pathname));
});
socket.current.on('message', m => {
console.log('message received: ' + JSON.stringify(m));
handleMessages([...messages, m]);
});
}, []);
return (...);
}
Edit: I've tried passing in a callback in my handleMessages function (functional updates as react calls them) but I still get the warning.
[![enter image description here][2]][2]
If you use the callback for handleMessages instead, there's no need to use the outer messages identifier (which would be in an old closure anyway).
socket.current.on('message', m => {
console.log('message received: ' + JSON.stringify(m));
handleMessages(messages => [...messages, m]);
});
In this case, you should using a callback in the handleMessages to update state. You can read more in here: https://reactjs.org/docs/hooks-reference.html#functional-updates
handleMessages((preMessage) => [...preMessage, m]);
If you're sure about this, add // eslint-disable-next-line before the line that shows the error.

How to implement this without triggering an infinite loop with useEffect

So I have a situation where I have this component that shows a user list. First time the component loads it gives a list of all users with some data. After this based on some interaction with the component I get an updated list of users with some extra attributes. The thing is that all subsequent responses only bring back the users that have these extra attributes. So what I need is to save an initial state of users that has a list of all users and on any subsequent changes keep updating/adding to this state without having to replace the whole state with the new one because I don't want to lose the list of users.
So far what I had done was that I set the state in Redux on that first render with a condition:
useEffect(() => {
if(users === undefined) {
setUsers(userDataFromApi)
}
userList = users || usersFromProp
})
The above was working fine as it always saved the users sent the first time in the a prop and always gave priority to it. Now my problem is that I'm want to add attributes to the list of those users in the state but not matter what I do, my component keeps going into an infinite loop and crashing the app. I do know the reason this is happening but not sure how to solve it. Below is what I am trying to achieve that throws me into an infinite loop.
useEffect(() => {
if(users === undefined) {
setUsers(userDataFromApi)
} else {
//Users already exist in state
const mergedUserData = userDataFromApi.map(existingUser => {
const matchedUser = userDataFromApi.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
})
setUsers(mergedUserData)
}
}, [users, setUsers, userDataFromApi])
So far I have tried to wrap the code in else block in a separate function of its own and then called it from within useEffect. I have also tried to extract all that logic into a separate function and wrapped with useCallback but still no luck. Just because of all those dependencies I have to add, it keeps going into an infinite loop. One important thing to mention is that I cannot skip any dependency for useCallback or useEffect as the linter shows warnings for that. I need to keep the logs clean.
Also that setUsers is a dispatch prop. I need to keep that main user list in the Redux store.
Can someone please guide me in the right direction.
Thank you!
Since this is based on an interaction could this not be handled by the the event caused by the interaction?
const reducer = (state, action) => {
switch (action.type) {
case "setUsers":
return {
users: action.payload
};
default:
return state;
}
};
const Example = () => {
const dispatch = useDispatch();
const users = useSelector(state => state.users)
useEffect(() => {
const asyncFunc = async () => {
const apiUsers = await getUsersFromApi();
dispatch({ type: "setUsers", payload: apiUsers });
};
// Load user data from the api and store in Redux.
// Only do this on component load.
asyncFunc();
}, [dispatch]);
const onClick = async () => {
// On interaction (this case a click) get updated users.
const userDataToMerge = await getUpdatedUserData();
// merge users and assign to the store.
if (!users) {
dispatch({ type: "setUsers", payload: userDataToMerge });
return;
}
const mergedUserData = users.map(existingUser => {
const matchedUser = action.payload.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
});
dispatch({ type: "setUsers", payload: mergedUserData });
}
return (
<div onClick={onClick}>
This is a placeholder
</div>
);
}
OLD ANSWER (useState)
setUsers can also take a callback function which is provided the current state value as it's first parameter: setUsers(currentValue => newValue);
You should be able to use this to avoid putting users in the dependency array of your useEffect.
Example:
useEffect(() => {
setUsers(currentUsers => {
if(currentUsers === undefined) {
return userDataFromApi;
} else {
//Users already exist in state
const mergedUserData = currentUsers.map(existingUser => {
const matchedUser = userDataFromApi.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
});
return mergedUserData;
}
});
}, [setUsers, userDataFromApi]);

Component unable to fetch data from Firebase when navigating to it for the first time or coming back

Background
I'm building an app which displays a number of stores in the home screen. They are shown in a carousel which is filled up with information from a Firestore Collection and Firebase Storage. The user can navigate into each store by pressing on them. The Home Screen display works just fine every single time, but when navigating to one store components come back as undefined. This is the way I'm fetching the data:
export default function StoreDetailMain ({route}) {
const { storeId } = route.params
const [store, setStore] = useState()
useEffect(() => {
const fetchQuery = async () => {
const storeData = await firebase.firestore()
.collection('stores/')
.doc(storeId)
.get()
.then(documentSnapshot => {
console.log('Store exists: ', documentSnapshot.exists);
if (documentSnapshot.exists) {
console.log('Store data: ', documentSnapshot.data());
setStore(documentSnapshot.data())
console.log(documentSnapshot.data())
}
});
}
fetchQuery()
}, [storeId])
Then I'm rendering the information within tags as in <Text>{store.value}</Text>.
Problem
Navigating once to the store will always return a Component Exception: undefined is not an object (evaluating 'store.value'). However if I cut the "{store.value}" tags it works just fine. Then I can manually type them in again and they render perfectly. Once I go back to the Home Screen and try to go into another store I have to do it all again. Delete the calls for information within the return(), save the code, reload the app and type them in again.
What I have tried
Sometimes, not always, Expo will give me a warning about not being able to perform a React state update on an unmounted component. I thought this might be the problem so I gave it a go by altering my useEffect method:
export default function StoreDetailMain ({route}) {
const { storeId } = route.params
const [store, setStore] = useState()
useEffect(() => {
let mounted = true;
if(mounted){
const fetchQuery = async () => {
const storeData = await firebase.firestore()
.collection('stores/')
.doc(storeId)
.get()
.then(documentSnapshot => {
console.log('Store exists: ', documentSnapshot.exists);
if (documentSnapshot.exists) {
console.log('Store data: ', documentSnapshot.data());
setBar(documentSnapshot.data())
console.log(documentSnapshot.data())
}
});
}
fetchQuery()
}
return () => mounted = false;
}, [storeId])
This would not solve the issue nor provide any variation.
Question
Is this due to the unmounting/mounting of components? If so, wouldn't the useEffect method take care of it? If anyone could provide an explanation/solution it would be very much appreciated.
Thanks in advance.
Edit 1:
When the application fails to render the information, it doesn't print into the console the document snapshot. When it can render the data, it does log it. Thus the change in title.
try giving it a initial value
const [ store, setStore ] = useState({value: ''})
or render it conditionally
{ store?.value && <Text>{store.value}</Text> }
secondly, route.params is defined? When you switching screens, did u make sure u pass the params? Switching from stack navigator to tab navigator for example, may drop the params.

Categories