Call and API inside useEffect in react native? - javascript

I'm calling an Api to get data but the data is really heavy. I'm wondering if i'm calling it in right place inside useEffect or should i call it somewhere else. I've put the console.log to check but the number of console.log exceeded the number of objects i have in the API. My code is :
const ProductsList = () => {
const [products, setProducts] = useState([]);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
getProducts().then((response) => {
if (isMounted) {
console.log('im being called');
setProducts(response);
setLoading(false);
}
});
return () => { isMounted = false; };
}, [products]);
return (
<View style={styles.container}>
{isLoading ? <ActivityIndicator /> : ((products !== [])
&& (
<FlatList
data={products}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => {
return (
<Item
style={{ marginLeft: 35 }}
name={item.name}
date={item.date}
address={item.adress}
/>
);
}}
/>
)
)}
</View>
);
};

It looks like your effect goes round in a circle:
On each render the effect will look at products to see if it has changed.
If it has changed it will call your effect which fetches new products.
When you get new products you update your products state.
This causes the effect to run again.
You probably only want to run that effect once, when the component mounts. In which case you can simply write:
useEffect(() => {
getProducts().then((response) => {
setProducts(response);
setLoading(false);
});
}, []); // <-- note the empty array, which means it only runs once when mounted
If that single API call is too heavy, then you need to look more at things like pagination in your requests, or modifying the response to return only the data you really need. But that's outside the scope of this question, I think.
Let me know if you have any questions.

Related

Fetched data from database not displaying in FlatList

Exactly as the title says I am fetching data from a database and trying to display the stored image URI as an image in my React Native application. I'm not entirely sure where the process is going wrong, but my array is being filled and nothing is being shown. I have tried hardcoding the data that's being fetched into my application and it is being displayed then. I have wrapped my view statement that the flatlist is nested into with the following: {arrayName && .... }. Here is some relevant code. The Test field appears, but the flatlist will not.
const [data, setData] = React.useState([]);
const userImgData = [];
{userImgData && <View style={{width: 300, height: 300}}>
<FlatList
data={userImgData}
keyExtractor={(item) => item.id}
key={item=> item.id}
renderItem={({ item }) => (
//<Text style={styles.headerText}>{item.id}</Text>
<Image source={{ uri: item.imageURI}} style={{ width: 200, height: 200 }} />
)}
/>
<Text>Test</Text>
</View>}
My array is set here...
function getData(){
axios.get('IP/imagefiles')
.then((response) => {
const myObjects = response.data;
setData(myObjects);
});
//console.log(data);
for(let i = 0; i < data.length; i++){
if(data[i].user == user){
userImgData.push(data[i]);
}else{
console.log('no data found!');
};
};
console.log(userImgData);
};
Here is how the data is coming across...
Edit: Changed userImgData to a state variable and got some results to show.
There appears to be a couple of issues present...
You're trying to loop over data before it is assigned a value. This is because the Axios request is asynchronous so the code after the request runs before the code in the then() callback.
userImgData isn't a state variable and even if it was, using push() won't trigger a state update and won't re-render your component.
Try using a memo hook to provide the filtered userImgData instead
const [data, setData] = useState([]);
const userImgData = useMemo(
() => data.filter((d) => d.user == user),
[data, user]
);
const getData = async () => {
setData((await axios.get("IP/imagefiles")).data);
};
You can check userImgData.length to conditionally render the list.

How do I display loading on top of all elements when URL in fetch is changed in react?

I have the code that fetches json data from a URL, and displays it, like so:
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [items, setItems] = useState([]);
const [url, setUrl] = useState(url);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setItems(result);
},
// Note: it's important to handle errors here instead of a catch() block
// so that we don't swallow exceptions from actual bugs in components.
(error) => {
setIsLoaded(true);
setError(error);
}
)
}, [url, setUrl]);
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <LoadingAnimation />;
} else {
return <BasicLayout items={items} setUrl={setUrl} />;
}
Where url is just an arbitrary url that fetches me some data. I have a custom LoadingAnimation function that simply displays loading when the page has not loaded. I pass setUrl down in the BasicLayout function because I want to change the URL parameters with the click of a button and update the data and display it. When the setUrl is triggered, the data updates as it should, with a 4-5 second delay. Now what additionally, while the data is updating during that 4-5 second delay, I want to display a different loading animation on top of all the elements and make the user unable to click on anything while the fetch is updating.
I have tried passing setIsLoaded into BasicLayout but that just replaced the entire screen and re-rendered every component. I especially don't want to re-render anything; just render the loading icon and hide it when everything has finished updating. How would I go about doing that?
at this moment, in your component, if statements return what React have to render. So you got few ways:
in this example if error change React will show BasicLayout or Error message but if isLoaded will change, LoadingAnimation will be hide/shown under the previous element.
useEffect(() => {
...
}, [url, setUrl]);
return (
<>
{error ? <div>Error: {error.message}</div> : <BasicLayout items={items} setUrl={setUrl} />}
{!isLoaded && <LoadingAnimation />}
</>
)
If you'll use right css styles you can simply make LoadingAnimation a modal(pop-up) but for modals, as for me, better to use React Portals
index.html
<div id='root' ></div>
<div id='modal' ></div>
Portal.jsx
const Portal = ({children}) => {
const portalNode = document.getElementById('modal')
return createPortal(children, portalNode)
}
Modal.jsx
const Modal = ({isActive, setIsActive, children}) => {
return (
<Portal>
{isActive && <div>{/* ...some code with children */}</div>}
</Portal>
)
}
So in your component it'll be something like this
useEffect(() => {
...
}, [url, setUrl]);
return (
<>
{error ?
<div>Error: {error.message}</div>
:
<BasicLayout items={items} setUrl={setUrl} />}
<Modal isActive={!isLoaded} ><LoadingAnimation /></Modal>
</>
)
and you can make error component modal too.
hope, it'll help:))

React-native application hanging when updating a parent from a child

I've a weird behavior here.
I'm trying to update a parent component from a child.
I've thus something like this for the child:
const LabelList = ({editable, boardLabels, cardLabels, size='normal', udpateCardLabelsHandler}) => {
return (
<DropDownPicker
labelStyle={{
fontWeight: "bold"
}}
badgeColors={badgeColors}
showBadgeDot={false}
items={items}
multiple={true}
open={open}
onChangeValue={(value) => udpateCardLabelsHandler(value)}
value={value}
setOpen={setOpen}
setValue={setValue} />
)
}
And, for the parent, something like this:
const CardDetails = () => {
const [updatedCardLabels, setUpdatedCardLabels] = useState([])
const [card, setCard] = useState({})
const [editMode, setEditMode] = useState(false)
// Handler to let the LabelList child update the card's labels
const udpateCardLabelsHandler = (values) => {
const boardLabels = boards.value[route.params.boardId].labels
const labels = boardLabels.filter(label => {
return values.indexOf(label.id) !== -1
})
console.log('updated labels',labels)
setUpdatedCardLabels(labels)
}
return (
<View style={{zIndex: 10000}}>
<Text h1 h1Style={theme.title}>
{i18n.t('labels')}
</Text>
<LabelList
editable = {editMode}
boardLabels = {boards.value[route.params.boardId].labels}
cardLabels = {card.labels}
udpateCardLabelsHandler = {udpateCardLabelsHandler} />
</View>
)
And, this just doesn't work: As soon as I try changing something in the DropDownPicker the application hangs. The console.log statement isn't even executed and no errors show up in my expo console.
What's strange is that if I change the updateCardLabels state to be a boolean for example, everything works ok (eg: the console.log statement is executed):
const [updatedCardLabels, setUpdatedCardLabels] = useState(false)
// Handler to let the LabelList child update the card's labels
const udpateCardLabelsHandler = (values) => {
const boardLabels = boards.value[route.params.boardId].labels
const labels = boardLabels.filter(label => {
return values.indexOf(label.id) !== -1
})
console.log('updated labels',labels)
setUpdatedCardLabels(true)
}
Please note that updatedCardLabels isn't used anywhere: it's a dummy variable that I'm just using to debug this issue (to make sure I was not ending in some endless render loop or something similar).
For the sake of completeness, here's what labels looks like at line console.log('updated labels',labels) (please not that I can only see this value when doing setUpdatedCardLabels(true) as otherwise, when the code does setUpdatedCardLabels(labels), the console.log statement is not executed, as mentioned earlier):
updated labels Array [
Object {
"ETag": "a95b2566521a73c5edfb7b8f215948bf",
"boardId": 1,
"cardId": null,
"color": "CC317C",
"id": 9,
"lastModified": 1621108392,
"title": "test-label",
},
]
Does anybody have an explanation for this strange behavior?
Best regards,
Cyrille
So, I've found the problem: It was a side effect of the DrowpDownPicker.
I've solved it by changing my child as follow:
const LabelList = ({editable, boardLabels, cardLabels, size='normal', udpateCardLabelsHandler}) => {
const [open, setOpen] = useState(false);
const [value, setValue] = useState(cardLabels.map(item => item.id));
const theme = useSelector(state => state.theme)
// Updates parent when value changes
useEffect(() => {
if (typeof udpateCardLabelsHandler !== 'undefined') {
udpateCardLabelsHandler(value)
}
}, [value])
return (
<DropDownPicker
labelStyle={{
fontWeight: "bold"
}}
badgeColors={badgeColors}
showBadgeDot={false}
items={items}
multiple={true}
open={open}
value={value}
setOpen={setOpen}
setValue={setValue} />
)

Firebase only fetches data on second or third attempt

I'm building an app with React and Firestore.
In one feature, I need to use some specific user data to query data from another collection, and show that on the app.
Specifically I want to use users.books, which returns an array, to query the collection books.
However, for some reason the users.books doesn't load on first render. It typically takes 2-3 renders to fetch the books.user data. This is despite the currentUserUID being loaded right away.
I've tried using a loading state as specified in How to wait for Firebase data to be fetched before progressing?, but to no avail.
Do I need to use the onSnapShot method?
Thanks for reading
My code
import 'firebase/firestore'
import { booksRef} from '../../App';
const ProfileScreen = ({ navigation }) => {
const currentUserUID = firebase.auth().currentUser.uid;
const [firstName, setFirstName] = useState('');
const [userBookTitles, setUserBookTitles] = useState([]);
const [userBooks, setUserBooks] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function getUserInfo(){
let doc = await firebase
.firestore()
.collection('users')
.doc(currentUserUID)
.get();
if (!doc.exists){
Alert.alert('No user data found!')
} else {
let dataObj = doc.data();
setFirstName(dataObj.firstName)
setUserBookTitles(dataObj.books)
console.log(userBookTitles)
}
}
getUserInfo();
}, [])
useEffect(() => {
async function getUserBooks() {
booksRef.where("title", "in", userBookTitles).onSnapshot(snapshot => (
setUserBooks(snapshot.docs.map((doc) => ({id: doc.id, ...doc.data()})))
))
}
setLoading(false);
getUserBooks()
}, [])
if (!loading) {
return (
<View style={styles.container}>
<Text> Hi {firstName} </Text>
<TouchableOpacity onPress={handlePress}>
<Text> Log out </Text>
</TouchableOpacity>
<Row
books={userBooks}
/>
</View>
);
} else {
return (
<View style={styles.container}>
<Text> Test </Text>
</View>
);
}
};
So it's worth noting that your setX methods may, or may not, complete in the sequence you have them in your code. Therefore, your booksRef call could be being made even though userBookTitles is an empty array. Which would explain why you're not getting any data on load.
You're setting userBookTitles in your first useEffect and the only other place I see you're using it is in your booksRef call. One easy fix would be to simple move booksRef inside the else statement of the first useEffect and simply pass it the userBookTitles there. This should help in solving your issue, if I understood it correctly.

Force a single re-render with useSelector

This is a follow-up to Refactoring class component to functional component with hooks, getting Uncaught TypeError: func.apply is not a function
I've declared a functional component Parameter that pulls in values from actions/reducers using the useSelector hook:
const Parameter = () => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter)
const dispatch = useDispatch();
const drawerOpen = useSelector(state => state.filterIconClick);
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
return (
prevState => ({
...prevState,
parameterCurrent: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
//some code describing an alert
});
})
.otherwise(function (err) {
alert(
//some code describing a different alert
);
});
}
);
};
const classes = useStyles();
return (
<div>
{drawerOpen ? (
Object.keys(parameterSelect).map((key, index) => {
return (
<div>
<FormControl component="fieldset">
<FormLabel className={classes.label} component="legend">
{key}
</FormLabel>
{parameterSelect[key].map((valKey, valIndex) => {
return (
<RadioGroup
aria-label="parameter"
name="parameter"
value={parameterCurrent[key]}//This is where the change should be reflected in the radio button
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
<FormControlLabel
className={classes.formControlparams}
value={valKey}
control={
<Radio
icon={
<RadioButtonUncheckedIcon fontSize="small" />
}
className={clsx(
classes.icon,
classes.checkedIcon
)}
/>
}
label={valKey}
/>
</RadioGroup>
);
})}
</FormControl>
<Divider className={classes.divider} />
</div>
);
})
) : (
<div />
)
}
</div >
)
};
export default Parameter;
What I need to have happen is for value={parameterCurrent[key]} to rerender on handleParameterChange (the handleChange does update the underlying dashboard data, but the radio button doesn't show as being selected until I close the main component and reopen it). I thought I had a solution where I forced a rerender, but because this is a smaller component that is part of a larger one, it was breaking the other parts of the component (i.e. it was re-rendering and preventing the other component from getting state/props from it's reducers). I've been on the internet searching for solutions for 2 days and haven't found anything that works yet. Any help is really apprecaited! TIA!
useSelector() uses strict === reference equality checks by default, not shallow equality.
To use shallow equal check, use this
import { shallowEqual, useSelector } from 'react-redux'
const selectedData = useSelector(selectorReturningObject, shallowEqual)
Read more
Ok, after a lot of iteration, I found a way to make it work (I'm sure this isn't the prettiest or most efficient, but it works, so I'm going with it). I've posted the code with changes below.
I added the updateState and forceUpdate lines when declaring the overall Parameter function:
const Parameter = () => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter);
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const dispatch = useDispatch();
const drawerOpen = useSelector(state => state.filterIconClick);
Then added the forceUpdate() line here:
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
return (
prevState => ({
...prevState,
parameterCurrent: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
//some code describing an alert
});
})
.otherwise(function (err) {
alert(
//some code describing a different alert
);
});
forceUpdate() //added here
}
);
};
Then called forceUpdate in the return statement on the item I wanted to re-render:
<RadioGroup
aria-label="parameter"
name="parameter"
value={forceUpdate, parameterCurrent[key]}//added forceUpdate here
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
I've tested this, and it doesn't break any of the other code. Thanks!

Categories