React useState without undefined values - reactjs

I'm novice with React and the state concept. So I fetch data from my database with axios. I put everything in a state (accomodation) and then I need to catch a field (country) into datas and use it into other function.
I succeed to catch everything but when I try to test it with a 'console.log' it appears that the two first result returns empty/undefined before having a result. Because the fact the two first attempts are empty/undefined, the other function doesn't work.
Anyone could help me please :)
Here are my code :
const { id } = useParams()
const [accomodation, setAccomodation] = useState('')
const getAcc = async () => {
const data = await axios.get(
`${process.env.REACT_APP_API_URL}api/v1/accomodations/${id}`
)
setAccomodation(data.data.data.accomodation)
}
useEffect(() => {
getAcc()
}, [])
const country = accomodation.country
console.log(country)

To go over what is happening here:
const [accomodation, setAccomodation] = useState('') the accomodation object is set to empty string.
useEffect(() => { getAcc() }, []) getAcc() is called upon render.
const country = accomodation.country; console.log(country) we are still waiting for getAcc() to finish, meanwhile, country is set to the 'country' property of an empty string which is underfined. undefined is printed.
setAccomodation(data.data.data.accomodation) getAcc() finally finishes, and accommodation is hopefully set to the object you intended.
What happens now?.. Nothing. Because you have not done anything to act on a state update. So one of the answers already posted here is on the right track. You need to act on the update of accommodation state.
This is how you trigger an effect on state update:
useEffect(() => {
if (accommodation.country) {
console.log(accomodation.country);
}
}, [accomodation])
Note that this useEffect() will be invoked whenever accommodation changes from its previous state. If the desired effect is not achieved it may be that accommodation is not the object you intended.

Related

How do I asynchronously update a variable from a paginated API using React hooks?

I'm currently trying to fetch all of the properties for an object from an API, and display them in a table. The API will return up to 10 results at a time, and will return a value nextPageToken in the response body if there are more results to be fetched. My goal is to fetch the first 10 results, immediately display them in the table, and add to the table as I continue to hit the API. This was my first attempt at a solution:
const getProperties = async (id) => {
const properties = await Api.getProperties(id);
setProperties(properties.properties);
if (properties.nextPageToken) loadMoreProperties(id, nextPageToken);
};
const loadMoreProperties = async (id, nextPageToken) => {
const properties = await Api.getProperties(id, nextPageToken);
setProperties(prevProperties => {return [...prevProperties, properties.properties]});
if (properties.nextPageToken) loadMoreProperties(id, properties.nextPageToken);
};
(Note that the above is a simplification; in practice, there's more logic in getProperties that doesn't need to be repeated on subsequent calls to the API)
The problem that I'm running into with this solution is that when I'm calling loadMoreProperties, the setProperties call isn't yet finished. How can I enforce that the call to loadMoreProperties only happens after setting the previous set of properties? Is there an overall better pattern that I can follow to solve this problem?
You can use useEffect to trigger the page loads as a reaction to a completed state change:
const [page, setPage] = useState(); // will be {properties, nextPageToken}
// load first page whenever the id changes
useEffect(() => {
Api.getProperties(id)
.then(page => setPage(page)));
}, [id]);
// load next page (if there is one) - but only after the state changes were processed
useEffect(() => {
if (page?.nextPageToken == null) return;
Api.getProperties(id, page.nextPageToken)
.then(page => setPage(page)));
}, [id, page]
);
// this will trigger the re-render with every newly loaded page
useEffect(()=> setProperties(prev => [...(prev || []), page.properties]), [page]);
The first effect will cause an update to the state variable page.
Only after the state change is completed, the second effect will be triggered and initiate the fetch of the second page.
In parallel, the third effect will perform the changes to the state variable properties, that your table component depends on, after each successful page load and page state update, triggering a re-render after each update.
I think you should pass a callback parameter to your "setProperties" method, to make the second call after the value has been updated, like this :
setProperties(properties.properties, () => {
if (properties.nextPageToken)
loadMoreProperties(id, nextPageToken);
);
Hope it can help
My solution involves removing the loadMoreProperties method itself.
While calling the getProperties for the 1st time, you can omit the nextPageToken argument.
getProperties = async(id,nextPageToken) {
var result = await Api.getProperties(id,nextPageToken);
this.setState((state)=>(state.properties.concat(result.properties)), ()=>{
// setState callback
if(result.nextPageToken) {
this.getProperties(id, nextPageToken);
}
});
}

problem with usestate in firebase to save data

I have a problem that I can not solve try a thousand ways and nothing
I happen to show
I generate a state:
const [totalComidas, setTotalComidas] = useState({})
After that I do a useEffect to bring me the data from firebase and I want to save it in the state but it saves it empty
useEffect(() => {
const resultadoComidas = []
db.collection('menu semanal')
.get()
.then((snap) => {
snap.forEach((doc) => {
const comida = doc.data()
// comida.id = doc.id
resultadoComidas.push(comida)
console.log(resultadoComidas[0])
})
setTotalComidas(resultadoComidas)
console.log(totalComidas)
})
}, [])
And these are my results in console
enter image description here
The first result in the console is before adding it to the state and the second is the new state, which seems strange to me because it brings the data correctly but when I assign it to the state it assigns it to me empty.
It's normal for the totalComidas to be empty when you log it inside useEffect because the result of your setTotalComidas won't be showed in that particular useEffect. If you want to see the change of your totalComidas you need to have another useEffect like so
useEffect(() => {
console.log(totalComidas)
})
If you wonder why this is the case then read this. It explains the behavior of useEffect

Howcome my state is not updating using react hooks and use Effect

My useEffect function is trying to fetch data from an API endpoint. The results resultAllTestTypes are currently logging fine.
However, I can't find out why the allTestTypes are coming back as undefined as I thought I had already set it in a state variable it should be able to log it to the console. But when I log the allTestTypes data it gives me this.
Code:
const [allTestTypes, setAllTestTypes] = useState([])
useEffect(() => {
async function onLoadCreateUnitTests() {
const results = await get('get_tables_autocomplete/b', user.user)
const resultsAllTestTypes = await get('get_all_test_types', user.user)
autoComplete.setTablesAutoComplete(results)
setAllTestTypes(resultsAllTestTypes)
console.log('resultAllTestTypes data ',resultsAllTestTypes.data);
console.log('allTestTypes data ',allTestTypes.data);
}
onLoadCreateUnitTests()
It's setting the state, you just have a console.log in a spot that's not particularly useful.
allTestTypes is a local const. It will never change, and that's not what setAllTestTypes is trying to do. When you set state, this tells react to render the component again. When that render occurs, you'll make a new call to useState, which will return the new value and assign it to a new local const. That new variable can be interacted with by code in the new render, but code from the previous render (such as your console.log) will never see the new value.
If you'd like to verify that the component is rerendering with a new value, move your console.log into the body of the component:
const [allTestTypes, setAllTestTypes] = useState([])
console.log('Rendering with', allTestTypes);
useEffect(() => {
async function onLoadCreateUnitTests() {
const results = await get('get_tables_autocomplete/b', user.user)
const resultsAllTestTypes = await get('get_all_test_types', user.user)
autoComplete.setTablesAutoComplete(results)
setAllTestTypes(resultsAllTestTypes)
}
onLoadCreateUnitTests()
});
cuz setAllTestTypes is async, so u can't get it immediately.
if u want to use it ,use the local variable resultsAllTestTypes instead

React prevent re-fetch when url params are unchanged

I have an ExperienceList component that fetches all items on mount. The functionality I am trying to achieve will occur when the user clicks into one particular experience, then navigates back to the ExperienceList. Currently, a fetch is made each time.
I want to do a simple check to see if the cityname from url params matches the cityname found in redux store. If it's the same, bypass the fetch. If not, do it.
Here are 2 approaches that I have tried, without luck:
First Approach
useEffect(() => {
if (props.match.params.cityname !== list[0].city) {
readList(apiRequestParams)
}
}, [])
With the above approach, I get the console error :
Cannot read property 'city' of undefined.
I am not 100% sure on why list is unavailable, since I am able to log it to the console.
Here is the Second approach:
useEffect(() => {
readList(apiRequestParams)
}, [props.match.params.cityname])
This approach does not work either, but I am more baffled as to why this is. As is my understanding, the variables appearing in the dependencies array of useEffect will cause the function to execute if and only if they are different between function calls. In this case, I can guarantee that props.match.params.cityname is the same between 2 different renders, yet a fetch is made each time.
Any insight on how I can optimize this process?
The function readlist is actually the dep of useEffect, you have to tell the useEffect. or you can change it to the cityname param.
const [cityName, setcityName] = useState('cityname');
useEffect(() => {
function getFetchUrl() {
return 'https://...' + cityName;
}
async function fetchData() {
const result = await readList(getFetchUrl());
setData(result.data);
}
fetchData();
}, [cityName]);
or
const fetchData = useCallback(() => {
readList("https://" + cityName);
}, [cityName]);
useEffect(() => {
fetchData()
},[fetchData])

Infinite loop in useEffect

I've been playing around with the new hook system in React 16.7-alpha and get stuck in an infinite loop in useEffect when the state I'm handling is an object or array.
First, I use useState and initiate it with an empty object like this:
const [obj, setObj] = useState({});
Then, in useEffect, I use setObj to set it to an empty object again. As a second argument I'm passing [obj], hoping that it wont update if the content of the object hasn't changed. But it keeps updating. I guess because no matter the content, these are always different objects making React thinking it keep changing?
useEffect(() => {
setIngredients({});
}, [ingredients]);
The same is true with arrays, but as a primitive it wont get stuck in a loop, as expected.
Using these new hooks, how should I handle objects and array when checking weather the content has changed or not?
Passing an empty array as the second argument to useEffect makes it only run on mount and unmount, thus stopping any infinite loops.
useEffect(() => {
setIngredients({});
}, []);
This was clarified to me in the blog post on React hooks at https://www.robinwieruch.de/react-hooks/
Had the same problem. I don't know why they not mention this in docs. Just want to add a little to Tobias Haugen answer.
To run in every component/parent rerender you need to use:
useEffect(() => {
// don't know where it can be used :/
})
To run anything only one time after component mount(will be rendered once) you need to use:
useEffect(() => {
// do anything only one time if you pass empty array []
// keep in mind, that component will be rendered one time (with default values) before we get here
}, [] )
To run anything one time on component mount and on data/data2 change:
const [data, setData] = useState(false)
const [data2, setData2] = useState('default value for first render')
useEffect(() => {
// if you pass some variable, than component will rerender after component mount one time and second time if this(in my case data or data2) is changed
// if your data is object and you want to trigger this when property of object changed, clone object like this let clone = JSON.parse(JSON.stringify(data)), change it clone.prop = 2 and setData(clone).
// if you do like this 'data.prop=2' without cloning useEffect will not be triggered, because link to data object in momory doesn't changed, even if object changed (as i understand this)
}, [data, data2] )
How i use it most of the time:
export default function Book({id}) {
const [book, bookSet] = useState(false)
const loadBookFromServer = useCallback(async () => {
let response = await fetch('api/book/' + id)
response = await response.json()
bookSet(response)
}, [id]) // every time id changed, new book will be loaded
useEffect(() => {
loadBookFromServer()
}, [loadBookFromServer]) // useEffect will run once and when id changes
if (!book) return false //first render, when useEffect did't triggered yet we will return false
return <div>{JSON.stringify(book)}</div>
}
I ran into the same problem too once and I fixed it by making sure I pass primitive values in the second argument [].
If you pass an object, React will store only the reference to the object and run the effect when the reference changes, which is usually every singe time (I don't now how though).
The solution is to pass the values in the object. You can try,
const obj = { keyA: 'a', keyB: 'b' }
useEffect(() => {
// do something
}, [Object.values(obj)]);
or
const obj = { keyA: 'a', keyB: 'b' }
useEffect(() => {
// do something
}, [obj.keyA, obj.keyB]);
If you are building a custom hook, you can sometimes cause an infinite loop with default as follows
function useMyBadHook(values = {}) {
useEffect(()=> {
/* This runs every render, if values is undefined */
},
[values]
)
}
The fix is to use the same object instead of creating a new one on every function call:
const defaultValues = {};
function useMyBadHook(values = defaultValues) {
useEffect(()=> {
/* This runs on first call and when values change */
},
[values]
)
}
If you are encountering this in your component code the loop may get fixed if you use defaultProps instead of ES6 default values
function MyComponent({values}) {
useEffect(()=> {
/* do stuff*/
},[values]
)
return null; /* stuff */
}
MyComponent.defaultProps = {
values = {}
}
Your infinite loop is due to circularity
useEffect(() => {
setIngredients({});
}, [ingredients]);
setIngredients({}); will change the value of ingredients(will return a new reference each time), which will run setIngredients({}). To solve this you can use either approach:
Pass a different second argument to useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
setIngredients({});
}, [timeToChangeIngrediants ]);
setIngrediants will run when timeToChangeIngrediants has changed.
I'm not sure what use case justifies change ingrediants once it has been changed. But if it is the case, you pass Object.values(ingrediants) as a second argument to useEffect.
useEffect(() => {
setIngredients({});
}, Object.values(ingrediants));
As said in the documentation (https://reactjs.org/docs/hooks-effect.html), the useEffect hook is meant to be used when you want some code to be executed after every render. From the docs:
Does useEffect run after every render? Yes!
If you want to customize this, you can follow the instructions that appear later in the same page (https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects). Basically, the useEffect method accepts a second argument, that React will examine to determine if the effect has to be triggered again or not.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
You can pass any object as the second argument. If this object remains unchanged, your effect will only be triggered after the first mount. If the object changes, the effect will be triggered again.
I'm not sure if this will work for you but you could try adding .length like this:
useEffect(() => {
// fetch from server and set as obj
}, [obj.length]);
In my case (I was fetching an array!) it fetched data on mount, then again only on change and it didn't go into a loop.
If you include empty array at the end of useEffect:
useEffect(()=>{
setText(text);
},[])
It would run once.
If you include also parameter on array:
useEffect(()=>{
setText(text);
},[text])
It would run whenever text parameter change.
I often run into an infinite re-render when having a complex object as state and updating it from useRef:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients({
...ingredients,
newIngedient: { ... }
});
}, [ingredients]);
In this case eslint(react-hooks/exhaustive-deps) forces me (correctly) to add ingredients to the dependency array. However, this results in an infinite re-render. Unlike what some say in this thread, this is correct, and you can't get away with putting ingredients.someKey or ingredients.length into the dependency array.
The solution is that setters provide the old value that you can refer to. You should use this, rather than referring to ingredients directly:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients(oldIngedients => {
return {
...oldIngedients,
newIngedient: { ... }
}
});
}, []);
If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect.
I believe they are trying to express the possibility that one could be using stale data, and to be aware of this. It doesn't matter the type of values we send in the array for the second argument as long as we know that if any of those values change it will execute the effect. If we are using ingredients as part of the computation within the effect, we should include it in the array.
const [ingredients, setIngredients] = useState({});
// This will be an infinite loop, because by shallow comparison ingredients !== {}
useEffect(() => {
setIngredients({});
}, [ingredients]);
// If we need to update ingredients then we need to manually confirm
// that it is actually different by deep comparison.
useEffect(() => {
if (is(<similar_object>, ingredients) {
return;
}
setIngredients(<similar_object>);
}, [ingredients]);
The main problem is that useEffect compares the incoming value with the current value shallowly. This means that these two values compared using '===' comparison which only checks for object references and although array and object values are the same it treats them to be two different objects. I recommend you to check out my article about useEffect as a lifecycle methods.
The best way is to compare previous value with current value by using usePrevious() and _.isEqual() from Lodash.
Import isEqual and useRef. Compare your previous value with current value inside the useEffect(). If they are same do nothing else update. usePrevious(value) is a custom hook which create a ref with useRef().
Below is snippet of my code. I was facing problem of infinite loop with updating data using firebase hook
import React, { useState, useEffect, useRef } from 'react'
import 'firebase/database'
import { Redirect } from 'react-router-dom'
import { isEqual } from 'lodash'
import {
useUserStatistics
} from '../../hooks/firebase-hooks'
export function TMDPage({ match, history, location }) {
const usePrevious = value => {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
const userId = match.params ? match.params.id : ''
const teamId = location.state ? location.state.teamId : ''
const [userStatistics] = useUserStatistics(userId, teamId)
const previousUserStatistics = usePrevious(userStatistics)
useEffect(() => {
if (
!isEqual(userStatistics, previousUserStatistics)
) {
doSomething()
}
})
In case you DO need to compare the object and when it is updated here is a deepCompare hook for comparison. The accepted answer surely does not address that. Having an [] array is suitable if you need the effect to run only once when mounted.
Also, other voted answers only address a check for primitive types by doing obj.value or something similar to first get to the level where it is not nested. This may not be the best case for deeply nested objects.
So here is one that will work in all cases.
import { DependencyList } from "react";
const useDeepCompare = (
value: DependencyList | undefined
): DependencyList | undefined => {
const ref = useRef<DependencyList | undefined>();
if (!isEqual(ref.current, value)) {
ref.current = value;
}
return ref.current;
};
You can use the same in useEffect hook
React.useEffect(() => {
setState(state);
}, useDeepCompare([state]));
You could also destructure the object in the dependency array, meaning the state would only update when certain parts of the object updated.
For the sake of this example, let's say the ingredients contained carrots, we could pass that to the dependency, and only if carrots changed, would the state update.
You could then take this further and only update the number of carrots at certain points, thus controlling when the state would update and avoiding an infinite loop.
useEffect(() => {
setIngredients({});
}, [ingredients.carrots]);
An example of when something like this could be used is when a user logs into a website. When they log in, we could destructure the user object to extract their cookie and permission role, and update the state of the app accordingly.
my Case was special on encountering an infinite loop, the senario was like this:
I had an Object, lets say objX that comes from props and i was destructuring it in props like:
const { something: { somePropery } } = ObjX
and i used the somePropery as a dependency to my useEffect like:
useEffect(() => {
// ...
}, [somePropery])
and it caused me an infinite loop, i tried to handle this by passing the whole something as a dependency and it worked properly.
Another worked solution that I used for arrays state is:
useEffect(() => {
setIngredients(ingredients.length ? ingredients : null);
}, [ingredients]);

Resources