Cannot access state variable inside function [duplicate] - javascript

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 2 years ago.
I'm building a messaging application. I'm retrieving the message log from my backend every 5 seconds with setTimeout and storing it in a state variable. I'm having it also scroll to the bottom of the chat window every timeout, but I am trying to make it only occur when there is a new message in the response object. The issue is that I cannot access the state variable inside my function that retrieves it. Ideally I would compare the response object's length to the state variable's current length to determine if there is an new response.
Here's my code:
import React from "react"
import axios from "axios"
export default function Messaging(props) {
const [messages, setMessages] = React.useState([])
const getMessages = () => {
const config = {
method: "get",
url: "localhost:3001/get-messages",
}
axios(config)
.then((response) => {
console.log(messages.length) // prints 0 in reference to the initial state value
if (response.data.get.records.length !== messages.length) {
// function to scroll to the bottom of the chat window
}
setMessages(response.data.get.records)
setTimeout(getMessages, 5000)
})
}
console.log(messages.length) // prints actual value correctly
// Initial retrieval of chat log
React.useEffect(() => {
getMessages()
}, [])
}
I'm open to better suggestions in handling the logic as well. Thanks in advance!

setMessages will update messages in the next render
With that in mind, you could create an Effect that whenever messages.length changes, scrolls to your element
import React from "react"
import axios from "axios"
export default function Messaging(props) {
const [messages, setMessages] = React.useState([])
React.useEffect(() => {
// store the id to clear it out if required
let timeoutId
const getMessages = () => {
const config = {
method: "get",
url: "localhost:3001/get-messages",
}
axios(config)
.then((response) => {
// store the messages
setMessages(response.data.get.records)
// here we tell to poll every 5 seconds
timeoutId = setTimeout(getMessages, 5000)
})
}
getMessages()
return () => {
if (timeoutId) {
clearTimeout(timeoutId)
}
}
}, [setMessages])
React.useEffect(() => {
// code here to scroll
// this would only trigger if the length of messages changes
}, [messages.length])
}
I also added the cleanup in your first effect, so when your component unmounts you cleanup the timeout and you avoid any extra request

Related

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 to input realtime data into NextJS?

I am just starting to learn Next.js framework.
I need help to solve a problem that I do not understand right now. In normal Vanilla JavaScript and React I can display the resulting API in HTML using the setInterval method.
My API changes data in every 3 seconds. I want to incorporate such variable data into my Next.js app.
Below I have combined the two APIs into a single props to carry data to other components.
export async function getServerSideProps(context) {
const [twoDApiRes, saveApiRes] = await Promise.all([
fetch(_liveResult),
fetch(_localTxt),
]);
const [twoDApi, saveApi] = await Promise.all([
twoDApiRes.json(),
saveApiRes.text(),
]);
// Regex
let csv_data = saveApi.split(/\r?\n|\r/);
// Loop through
const retrieveData = csv_data.map((el) => {
let cell_data = el.split(',');
return cell_data;
});
return {
props: { twoDApi, retrieveData },
};
}
The main thing to know is that you want to change the data every three seconds in Next.js getServerSideProps.
You can use useEffect hook to refresh data every 3 seconds.
// This function will return Promise that resolves required data
async function retrieveData() {
// retrieves data from the server
}
function MyPage() {
const [data, setData] = useState();
const [refreshToken, setRefreshToken] = useState(Math.random());
useEffect(() => {
retriveData()
.then(setData)
.finally(() => {
// Update refreshToken after 3 seconds so this event will re-trigger and update the data
setTimeout(() => setRefreshToken(Math.random()), 3000);
});
}, [refreshToken]);
return <div>{data?.name}</div>
}
Or you can use react-query library with {refetchInterval: 3000} options to refetch data every 3 seconds.
Here's an example for using react-query.

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

React try again if error when fetching data from API

Here's my page:
function MyAssets() {
const [assetData, setAssetData] = useState({})
const [assetArray, setAssetArray] = useState([{symbol:'', amount:''}])
and its component below.
I'm trying to fetch data in this component, I set a setInterval to run it every second.
but requests are too many, it sometimes send back status code 429 and fail.
So I add an if statement, if I fetched data successfully, I clear this setInterval .
This one failed too, it seems every time it triggers setState, the whole component render again, and my checkState become false, and triggers again.
How should I stop this component once I fetch date from API successfully?
//React component
function AssetRow(props) {
const [price, setPrice] = useState(null)
const [checkState, setCheckState] = useState(false)
const fetchStockPrice = async()=>{
const data = await api.getStock(props.item.symbol)
setPrice(data.data.latestPrice)
}
//I try to run fetchStockPrice() every 1 second here
useEffect(()=>{
const doWork = setInterval(() => {
if (checkState === false){
fetchStockPrice()
setCheckState(true)
} else if (checkState === true) {
clearInterval(doWork)
}
}, 1000)
return () => clearInterval(doWork)
}, [])
return (
<h1>{price}</h1>
)
}

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