React native no re-render after updating state array - reactjs

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.

Related

Using Hooks API: does React respect setState order?

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)

How to apply animation in react-native?

I want to add animation to this search bar.
when the user touches the search bar it size decreases and again increases and gets to its default size(like animation in popups)
This is my code
<View style={{flexDirection:'row',alignSelf:'center'}}>
<TextInput
onChangeText={(text) => setSearch(text)}
onFocus={()=>{
setSize('92%');
setInterval(()=>{setSize('95%')},1000)
}}
placeholder="Search"
style={{...styles.searchbox,width:size}}
></TextInput>
</View>
I am currently trying to change width..
Firstly, I suggest you to take a look at RN animated documentation, maybe it will help you to understand better how the animations work.
Also, it depends on what you're having there: a class component or a function component.
If you're using a function component, you could do it like this:
Creating a custom hook, called, let's say useAnimation(), which would look something like this
export const useAnimation = ({ doAnimation, duration, initialValue, finalValue }) => {
const [animation, setAnimation] = useState(new Animated.Value(initialValue))
useEffect(() => {
Animated.spring(animation, {
toValue: doAnimation ? initialValue : finalValue,
duration,
bounciness: 8,
useNativeDriver: false
}).start();
}, [doAnimation]);
return animation
}
As it is said in the documentation, you could animate only Animated components, and for example if you want to have an animated View, the tag will be <Animated.View> {...} </Animated.View, but for the <TextInput> we have to create the animated component:
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
and combining the first 2 steps
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
const [collapsed, setCollapsed] = useState(true)
const animation = useAnimation({ doAnimation: collapsed, duration: 300, initialValue: 20, finalValue: 200 });
const onFocusText = () => setWidth(false)
return (
<AnimatedTextInput
onFocus={onFocusText}
placeholder={"Search something"}
style={{width: animation, height: 50, borderColor: 'gray', borderWidth: 1, borderRadius: 4, padding: 10}}
/>
)
Also, if you're having a class component, you could have a method which will start the animation (but don't forget about the step 2 which is essential)
private size: Animated.Value = new Animated.Value(COLLAPSED_VALUE)
get resizeInputWidth(): Animated.CompositeAnimation {
return Animated.timing(this.size, {
toValue: EXPANDED_VALUE,
duration: 500,
})
}
startAnimation = () => this.resizeInputWidth.start()
<AnimatedTextInput
onFocus={this.startAnimation}
style={{ width: this.size }}
/>

Clearing map layers with react-leaflet and hooks

I'm building a custom plugin for react-leaflet to locate the user using leaflet's locate method.
It works basically, but with the one problem of the layer not clearing between turning location off and back on again. Each time the locate button is toggled, locate should start fresh.
Here is a codesandbox of the problem. As you toggle the button, the circle becomes darker as the layers are stacked on top of each other.
Here is the component:
import React, { useEffect, useState, useRef } from 'react'
import L from 'leaflet'
import { useLeaflet } from 'react-leaflet'
import LocationSearchingIcon from '#material-ui/icons/LocationSearching';
import MapButton from './MapButton'
function Locate() {
const { map } = useLeaflet();
const [locate, setLocate] = useState(false);
function toggleLocate() {
setLocate(!locate)
}
console.log(locate)
const layerRef = useRef(L.layerGroup());
useEffect(() => {
if (locate) {
map.removeLayer(layerRef.current)
map.locate({ setView: false, watch: true, enableHighAccuracy: true }) /* This will return map so you can do chaining */
.on('locationfound', function (e) {
L.circleMarker([e.latitude, e.longitude], {
radius: 10,
weight: 3,
color: 'white',
fillColor: 'blue',
fillOpacity: 1
}).addTo(layerRef.current);
L.circle([e.latitude, e.longitude], e.accuracy / 2, {
weight: 1,
color: 'blue',
fillColor: 'blue',
fillOpacity: 0.2
}).addTo(layerRef.current);
window.localStorage.setItem('accuracy', e.accuracy)
map.setView([e.latitude, e.longitude], 16)
layerRef.current.addTo(map)
})
.on('locationerror', function (e) {
alert("Location error or access denied.");
})
} if (!locate) {
map.stopLocate();
map.removeLayer(layerRef.current);
}
}, [locate, map]);
return (
<MapButton
title={locate ? 'Click to disable location' : 'Click to locate'}
onClick={toggleLocate}
left
top={102}
>
<LocationSearchingIcon fontSize="small" style={{ color: locate ? 'orange' : '' }} />
</MapButton>
)
}
export default Locate
I would appreciate any solution or tips to stop the layers stacking, and clear properly went the button is toggled. Thanks
As Falke Design's mentions in a comment above:
Add the circle to a featureGroup and then every time locationfound is fired, you call featuregroup.clearLayers() and then add the circle new to the featureGroup
This solved the problem.
The working codesandbox is here

React component remounts when state object is reordered

I have a react-redux application, and am using react-transition-group for transitions when child components position changes in a state change. However, if the order of my redux state object changes, components will unmount and then remount, rather than updating. This prevents the component from smoothly transitioning to its new position. Is there a way around this without introducing new state?
My parent component looks like this:
class ParentComponent extends Component {
...
render() {
return (
<div>
<div>
<CSSTransitionGroup>
<ChildComponents childComponentsToRender=
{this.props.someChildComponents}/>
<CSSTransitionGroup/>
</div>
</div>
)}}
ChildComponents looks like this:
export default function ChildComponents(props) {
const childrenToReturn = _.map(props.childComponentsToRender,
(childComponent) =>
<ChildComponent
key={childComponent.uniqueId}
position={childComponent.position} .../>
return (
<Fragment>
{childrenToReturn}
</Fragment>
)
}
And an individual ChildComponent looks like this:
export default function ChildComponent(props) {
return (
<div style={{transform: `translate(${props.position.x}px,
${props.position.y}px)`}}>
</div>
)
}
My redux state that renders the components looks like:
{
...,
someChildComponents: {
childComponent1: {
id: someUniqueId,
position: {x: int, y: int}
},
childComponent2: {
id: someUniqueId,
position: {x: int, y: int}
},
...
},
...
}
The problem would occur if an action was dispatched and the state would change from, for instance:
{
someChildComponents: {
childComponent1: {
id: someUniqueId,
position: {x: 100, y: 100}
},
childComponent2: {
id: someUniqueId,
position: {x: 200, y: 200}
}
}
}
To:
{
someChildComponents: {
childComponent2: {
id: someUniqueId,
position: {x: 300, y: 300}
},
childComponent1: {
id: someUniqueId,
position: {x: 150, y: 150}
}
}
}
If you need to relate on specific position of element, you better need array, not object. But I don't recommend to convert someChildComponents to array. In most cases, object is preferred to array when storing distinct data.
You may create array on the fly based on your someChildComponents object, for example using selectors. The only thing you need is some unique identifier within someChildComponents object, which can be used to sort your array.
Here is example code
const getChildrenComponents = state => state.someChildComponents;
export function makeArrayFromChildrenComponents() {
return createSelector(
[getChildrenComponents],
(ChildrenComponents) => (
Object.keys(ChildrenComponents).map(key => ChildrenComponents[key])
.sort((e1, e2) => {
if (e1.id === e2.id) return 0;
return e1.id < e2.id ? -1 : 1;
});
)
)
}
Then use it to provide stable array of children components to ParentComponent like this
const makeMapStateToProps = () => {
const getChildrenComponentsArray = makeArrayFromChildrenComponents();
const mapStateToProps = (state) => ({
ChildrenComponents: getChildrenComponentsArray(state)
// ... other mapped props
});
return mapStateToProps;
}
Selector will update ChildrenComponents whenever state changes. But array itself will hold the same order of elements.
You may also take a look at diffing algorithm used by React to decide whether to build new tree or hold existing.

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