Using Hooks API: does React respect setState order? - reactjs

I have fairly nonexistent knowledge in react but I'm learning as I go. I learned the basics back in school, with class components (classic React), but now I'm delving into the Hooks API (mainly because I find it easier to learn and manage, although there seems to be more tricks involved regarding async behavior). So my question might seem silly.
I found this thread regarding setState behavior on the same topic, but this is regarding class components.
In my current application, I'm trying to set three different states using an event handler. It seems that the last state is set immediately, whereas the other two states remain undefined for a bit before changing to a real value. I'm using React-Native components for mobile development, so you'll see snippets in the code such as <SafeAreaView>.
export default App = () => {
const [ destLong, setDestLong ] = useState();
const [ destLat, setDestLat ] = useState();
const [ startNav, setStartNav ] = useState(false);
const [ locations, setLocations ] = useState([
{
name: 'IKEA',
long: '-74.00653395444186',
lat: '40.68324646680103',
},
{
name: 'JFK Intl. Airport',
long: '-73.78131423688552',
lat: '40.66710279890186',
},
{
name: 'Microcenter',
long: '-74.00516039699959',
lat: '40.67195933297655',
}
]);
const startNavigation = (goinglong, goinglat) => {
setDestLong(goinglong);
setDestLat(goinglat);
setStartNav(true);
}
return (
<SafeAreaView style={styles.container}>
{ startNav ?
<MapView
destLong = {destLong}
destLat = {destLat}
/>
:
<View style={styles.buttonContainer}>
<ScrollView>
{
locations.map((location, i) => {
return(
<Card
style={styles.card}
key={i}
title={ location.name }
iconName="home"
iconType="Entypo"
description={ location.long + ", " + location.lat }
onPress={() => startNavigation(location.long, location.lat)}
/>
);
})
}
</ScrollView>
</View>
}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
buttonContainer: {
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center'
},
logo: {
width: '50%',
height: '50%',
resizeMode: 'contain'
},
card: {
marginBottom: 10,
}
});
This throws an error, because MapView is expecting destLong and destLat to render properly. When I console log inside my startNavigation function, it seems that it immediately updates the state for startNav to true onPress, but destLong and destLat remain undefined for a few cycles before being set.
I've tried a different approach like this:
useEffect(() => {
setStartNav(true);
}, [destLong]);
const startNavigation = (goinglong, goinglat) => {
setDestLong(goinglong);
setDestLat(goinglat);
}
But it just crashes the app (my guess is infinite loop).
I've also tried removing the startNav state altogether and rendering <MapView> on destLong like this
{ destLong ?
<MapView
destLong = {destLong}
destLat = {destLat}
/>
:
<View style={styles.buttonContainer}>
...
</View>
}
But that did not work either.
Which brings me to this question: does the Hooks API respect the order of setState, or is each other carried out asynchronously? From my understanding it's the latter. But then, how do you handle this behavior?

I'm adding my comment here as well since I am unable to add proper formatting to my comment above.
Setting a state via useState is actually asynchronous, or rather the state change is enqueued and it will then return its new value after a re-render. This means that there is no guarantee in what order the states will be set. They will fire in order, but they may not be set in the same order.
You can read more here: https://dev.to/shareef/react-usestate-hook-is-asynchronous-1hia, as well as here https://blog.logrocket.com/a-guide-to-usestate-in-react-ecb9952e406c/#reacthooksupdatestate
In your case I would use useState and useEffect like this:
useEffect(() => {
if(destLong && destLat && !startNav) {
setStartNav(true);
}
}, [destLong, destLat, startNav]);
const startNavigation = (goinglong, goinglat) => {
setDestLong(goinglong);
setDestLat(goinglat);
}
With that said, I think you could further simplify your code by omitting the startNav state altogether and update your conditional render:
{ (destLat && destLong) ?
<MapView
destLong = {destLong}
destLat = {destLat}
/>
:
<View style={styles.buttonContainer}>
...
</View>
}
The above should have the same effect since you have two states that are undefined to begin with, and when they are both defined you want to render something and use their values.
And if you want to display the options again you can set the states to undefined again by doing setDestLat(undefined) and setDestLong(undefined)

Related

React Native Filter by Category using Button or Touchableopacity

How to Filter the record based status on the button click in the react native ?
Here is the data I have:
[
{
"id":28,
"Title":"Sweden",
"Status":1
},
{
"id":56,
"Title":"USA",
"Status":1
},
{
"id":89,
"Title":"England"
"Status":1
},
{
"id":89,
"Title":"England"
"Status":2
},
{
"id":89,
"Title":"England"
"Status":2
},
{
"id":89,
"Title":"England"
"Status":3
}
]
Here I have id, title, status..Now I need to filter this according to the status
Critique
Although your question is very specific in terms of programming capabilities, it is very lacking in terms of context.
Keep in mind that people answering these questions do so in their own time, questions like yours, are sometimes really hard to cypher.
missing key elements in your question:
At what stage should the filtering be done?
Did you already try something that didn't work?
Do you already have a running react-native app? or are you starting from scratch?
Going with react-native, are you using hooks? or are extending the React.Component class?
Is the issue you are having even react-native related? or just JavaScript? because with a quick google search, you would've landed here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
I could go on, but the bottom line, help us, help you,by painting a picture around your current situation, it will help you get faster, better answers.
I hop I won't discourage from participating in the future, but understand that these kind of question really make it hard to understand, what you need.
Answer
Now, to try to answer what I think you need answering.
like #nithinpp already stated, filtering arrays in JavaScript can be done with the Array.prototype.filter() method, it usage usually looks like so:
// Example taken from the https://developer.mozilla.org/ website
const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];
const result = words.filter(word => word.length > 6);
console.log(result);
// expected output: Array ["exuberant", "destruction", "present"]
Within The React/React-Native realms, this method would have to be used, either inside a react(-native) component, or inside a utility-service/reducers/saga that filters said data, and passes it on to the component through props.
For the sake of this example, I'll be using react-hooks, and do the filtering inside a component, such a solution could look like so:
Playable Online Version using Expo Snack: https://snack.expo.dev/yTte5nPud
//This is an example of online Emulator by https://aboutreact.com
import React, {useState, useMemo} from 'react';
import { Text, View, StyleSheet, Button, FlatList } from 'react-native';
// Item Component To render single items
function Item(props) {
return (
<View style={styles.item}>
<Text>{props.item.id}: {props.item.Title} ({props.item.Status})</Text>
</View>
);
}
// Main App
const App = () => {
// There are a lot of different ways to keep this info,
// for the sake of this answer, I've put it in a useState hook
const [fullList, setFullList] = useState([
{ "id":28, "Title":"Sweden", "Status":1 },
{ "id":56, "Title":"USA", "Status":1 },
{ "id":89, "Title":"England", "Status":1 },
{ "id":89, "Title":"England", "Status":2 },
{ "id":89, "Title":"England", "Status":2 },
{ "id":89, "Title":"England", "Status":3 }
]);
// Keep a statue of the current selected status
const [status, setStatus] = useState('NONE')
// the filtered list, cached with useMemo
// the callback is call each time the status or the fullList changes
const filteredList = useMemo(
() => {
if (status === 'NONE' ) return fullList
return fullList.filter(item => status === item.Status)
},
[status, fullList]
)
// the onClick Method is a method that returns a method, which
// updates the state based on the predefined status
const onClick = (status) => () => {
setStatus(status)
}
// render list using flat list, and the filter bar using standard buttons
return (
<View style={styles.container}>
<Text>Selected Status: {status}</Text>
<View style={styles.filterBar}>
<Button title="Clear" onPress={onClick('NONE')} />
<Button title="Status 1" onPress={onClick(1)} />
<Button title="Status 2" onPress={onClick(2)} />
<Button title="Status 3" onPress={onClick(3)} />
</View>
<FlatList
style={styles.list}
renderItem={Item}
keyExtractor={(item) => item.id}
data={filteredList}
/>
</View>
);
};
export default App;
// some basic styling
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 8,
backgroundColor: 'white',
},
list: {
height: '100%',
width: '100%'
},
filterBar: {
flexDirection: 'row',
// flex: 0.2,
height: 40,
},
item: {
flex: 1,
justifyContent: 'flex-start',
padding: 8,
backgroundColor: 'white',
}
});
Summary
Now This answer assumes a lot of things, I still don't know the extent of your knowledge regarding react-native, or if you even know how to use react-native components such as the FlatList, and this answer should not start explaining those as well, thus might end up confusing you and giving you an answer to something you haven't even asked, which I hope is not the case.
Anyways, I hope this helps, and maybe keep these points in mind in your next question
You can use Array Filter to filter your data based on your condition.
For example,
function filterArray(array, status){
return array.filter(item => item.Status === status);
}
const DATA = [ { "id":28, "Title":"Sweden", "Status":1 }, { "id":56, "Title":"USA", "Status":1 }, { "id":89, "Title":"England", "Status":1 }, { "id":89, "Title":"England", "Status":2 }, { "id":89, "Title":"England", "Status":2 }, { "id":89, "Title":"England", "Status":3 } ];
function filterArray(array,status){
return array.filter(item => item.Status === status);
}
const statusOneArray = filterArray(DATA, 1);
console.log(statusOneArray);

React native no re-render after updating state array

I have the following code (full example):
import React, { useState, useEffect } from 'react';
import { SafeAreaView, View, Button, StyleSheet, Animated } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
const App = () => {
const [blocks, setBlocks] = useState([]);
const CreateBlockHandler = () => {
let array = blocks;
array.push({
x: new Animated.Value(0),
y: new Animated.Value(0)
});
setBlocks(array);
RenderBlocks();
};
const MoveBlockHandler = (index, event) => {
Animated.spring(blocks[index].x, { toValue: event.nativeEvent.x }).start();
Animated.spring(blocks[index].y, { toValue: event.nativeEvent.y }).start();
};
const RenderBlocks = () => {
return blocks.map((item, index) => {
return (
<PanGestureHandler key={index} onGestureEvent={event => MoveBlockHandler(index,event)}>
<Animated.View style={[styles.block, {
transform: [
{ translateX: item.x },
{ translateY: item.y }
]
}]} />
</PanGestureHandler>
)
});
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.pancontainer}>
<RenderBlocks />
</View>
<Button title="Add block" onPress={CreateBlockHandler} />
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
pancontainer: {
width: '95%',
height:'75%',
borderWidth: 1,
borderColor: 'black'
},
block: {
width: 50,
height: 50,
backgroundColor: 'black'
}
});
export default App;
What does this code do? It's a big square, and a button below it. When I click on the button, a new black square (50x50) is made in the big square. I do this by creating a new array element (the array = blocks). This is done in the function CreateBlockHandler. This does not work correctly!
The function MoveBlockHandler makes the little squares movable. This works!
What does not work? When I create a new black square, the black square is not rendered on the screen. Only when I refresh, the square is rendered. The square is created through CreateBlockHandler, because when I do a console.log(blocks) in that function, I can see that a new array element is added.
How can I force this code to do a full re-render with all the array elements? I tried to wrap the render of the square in a separate function (RenderBlocks) and I'm calling this function every time a new square is made (last line in CreateBlockHandler). The function is called (I can check this with a console.log()) but no squares are rendered.
When you assign blocks to array the reference gete copied which mutates the state, so it doesn't re-render on setState.
const CreateBlockHandler = () => {
let array = [...blocks];
array.push({
x: new Animated.Value(0),
y: new Animated.Value(0)
});
setBlocks(array);
RenderBlocks
There are multiple issues with your code.
As kooskoos pointed out, your state remains referentially equal (it's the same array, only the elements change). This will not trigger re-render.
Also, you are manipulating state of the App component. RenderBlocks component's props and state remain unchanged which implies that they don't need to be re-rendered. Since the component is an anonymous function and is recreated during every render of App, it probably gets re-rendered anyways.
In addition, you are directly calling RenderBlocks, which looks like a component. That is unnecessary and will do nothing here, but if it had any hooks, it would cause problems.
You should probably also conform to the convention that components are PascalCase capitalised and callbacks snakeCase capitalised.

React native: Undefined is not an object

I'm making a react native app with expo and I'm having some trouble parsing the response data from the api. Below is a sample response from the api and my goal is to retrieve main from the weather array. I can log weather array just fine but when I put state.weather[0].main it throws the undefined object error.
Object {
"clouds": Object {
"all": 20,
},
...,
"weather": Array [
Object {
"description": "few clouds",
"icon": "02d",
"id": 801,
"main": "Clouds",
},
],
}
Below is my code, I fetch from the api on useEffect() hook. At first the logs shows undefined but after fetching it logs correctly except for state.weather[0].main. I just started react native coming from Android, I hope you guys could help me, I'm stuck for a day now. Thanks
const HomeScreen = ({ navigation }) => {
const { state: { defaultCity } } = useContext(CitiesContext);
const { state: { weather }, fetchWeather } = useContext(WeatherContext);
useEffect(() => {
if (defaultCity) {
fetchWeather(defaultCity);
}
}, []);
console.log(weather);
console.log(typeof (weather[0]));
console.log(weather[0]);
console.log(weather[0].main);
return (
<View style={styles.container}>
<LinearGradient
// {...weatherBgProvider[weather.weather.main]}
colors={['#000', '#fff']}
style={styles.gradient}
>
<SafeAreaView
style={{
position: 'relative'
}}
forceInset={{ top: 'always' }}
>
<View style={{ marginTop: Header.HEIGHT, flex: 1 }}>
<Text >{JSON.stringify(state)}</Text>
</View>
</SafeAreaView>
</LinearGradient>
</View>
);
};
If you are trying to log data in your render, you can do it like this:
<View>{This.state.weather[0] && console.log(this.state.weather[0]) }</View>
So you check it to exist before you log data
Hope you get the idea and works for you
update
If your main not working, first try to parse your object into array like this:
first way use lodash (npm i --dev lodash) and import it in your project
Import _ from "lodash";
Const newdata= _.compact(Object.values(yourobject))
And then you can use newdata and i think your problem will be solved
second way use _.mapKeys(yourdata,"id") and put new data in your variable like const newdata
I hope it works

How to create a blinking react View

Can someone help me out how I can create a blink-able react-native component?
So basically, this is what I have done
class Blinkable extends PureComponent {
state = {
blinkComponentVisibility: false
}
blink () {
this.setState({blinkComponentVisibility: ! blinkComponentVisibility})
console.log(this.state)
}
componentDidMount = () => {
setTimeout(() => {this.blink}, 3000)
}
render () {
if (i === currentProgress) {
if (this.state.blinkComponentVisibility) {
progressBarArray.push(
<View
style={{
width: widthOfIndividualBlog,
backgroundColor: colorOfProgressBar,
height: heightOfProgressBar
}}
key={i}
></View>)
}
}
return (
<View>
<View style={{display: 'flex', flexDirection: 'row'}}>{progressBarArray}</View>
</View>
)
}
}
With the above code, I was expecting my component to blink but nothing happens rather I see the following logs in console
RCTLog.js:47 Could not locate shadow view with tag #363, this is
probably caused by a temporary inconsistency between native views and
shadow views
Can someone please help me in figuring out what I could be doing wrong?
this.setState({blinkComponentVisibility: ! blinkComponentVisibility}) should be this.setState({blinkComponentVisibility: ! this.state.blinkComponentVisibility})
and in your set timout you need to call the function
setTimeout(() => this.blink(), 3000)

How to setState of a particular index in an array React Native

I have an array of objects that I am currently mapping over to generate as buttons. When clicked, I want the background color of the specific button the user clicks on to change color ( I want it to toggle on, like a switch, so I can eventually save to async storage). Right now when the user clicks, all buttons change color. I'm not quite sure how I should handle this.setState in the selectMetric function.
import React, {Component} from 'react';
import {View, Text, ScrollView} from 'react-native';
import {Button} from 'react-native-elements';
const RISK_DATA = [
{id: 1, text: 'cats', flag: false, buttonColor: null},
{id: 2, text: 'dogs', flag: false, buttonColor: null},
]
class IssueSelectionScreen extends Component {
state = {flag: false, buttonColor: null}
selectMetric = (index) => {
for (let i=0; i < RISK_DATA.length; i++) {
if (index === (RISK_DATA[i].id - 1)) {
console.log("RISK_DATA:", RISK_DATA[i]); // logs matching id
// ------------------------------------------------------
// Problem setting correct state here:
RISK_DATA[i].buttonColor = this.setState({flag: true, buttonColor: '#03A9F4'})
// this.setState({flag: true, buttonColor: '#03A9F4'})
// this.setState({update(this.state.buttonColor[i], {buttonColor: {$set: '#03A9F4'}}) })
// ----------------------------------------------------------
}
}
}
showMetric() {
return RISK_DATA.map((metric, index) => {
return (
<View key={metric.id}>
<Button
raised
color={'black'}
title={metric.text}
borderRadius={12}
onPress={() => this.selectMetric(index)}
backgroundColor={this.state.buttonColor}
>
{metric.text}
</Button>
<Text>{/* intentionally blank*/} </Text>
</View>
)
})
}
render() {
return(
<ScrollView style={styles.wrapper}>
<View style={styles.issues}>
{this.showMetric()}
</View>
</ScrollView>
);
}
}
const styles = {
issues: {
justifyContent: 'center',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'flex-start',
marginTop: 10,
justifyContent: 'space-between',
},
wrapper: {
backgroundColor: '#009688'
}
}
export default IssueSelectionScreen;
so the short answer to your question would look something like this:
class IssueSelectionScreen extends Component {
constructor(props) {
super(props);
this.state = {
data: cloneDeep(RISK_DATA),
};
}
selectMetric = (index) => {
const tempData = cloneDeep(this.state.data);
tempData[index].flag = !tempData[index].flag;
this.setState({ data: tempData });
}
showMetric() {
return this.state.data.map((metric, index) => {
// same
})
}
render() {
// same
}
}
It involves putting the whole array of buttons into state since the state of those buttons is what can change. You could also maintain the flags as an array in state and keep the button info as a separate constant
This solution uses cloneDeep (from lodash) to prevent the code from mutating the state of the objects but you could probably also do it with this.state.data.map and creating new objects (which works as long as your objects aren't deeply nested).
If you're using Redux, the list would probably come into the component as a prop, then selectMetric would be dispatching an action to update the flag in Redux.
For anyone else viewing this post, the answer above is very helpful. To add a few last remarks, if you're trying to get the buttons to light up I added a simple if else to selectMetric:
if (tempData[index].flag) {
tempData[index].buttonColor = '#03A9F4';
console.log('tempData true:', tempData);
} else {
tempData[index].buttonColor = null;
console.log('tempData false:', tempData);
}
and updated the backgroundColor property on Button in showMetric with:
backgroundColor={this.state.data[index].buttonColor}

Resources