State update only affects component if it affects parents' style - react-native

I have an app that lets the user scan for and connect to BLE devices. If the user taps a scan result, I want a spinner to show next to the device while the connection is in progress.
I am using Redux to keep track of discovered and connected devices. The main App component is connect with redux-react and passes the relevant bits of state to its children.
One of these children is the list of scan results, which in turn passes one device to each of its entries. The scan list items look like this:
class DeviceListItem extends Component{
render = () => {
return <TouchableOpacity onPress={this.props.onPress}>
<Card>
<View style={{justifyContent: "space-between", flexDirection: "row", flex:1, width:"100%", alignItems: this.props.item.pendingActions !== 0?"flex-start":undefined}>
<Text>{this.props.item.name}</Text>
<ActivityIndicator animating= {this.props.item.pendingActions !== 0} color={Colors.accent} size="small"/>
</View>
</Card>
</TouchableOpacity>
}
}
Now, in the style of the View component you might notice this peculiar snippet of code:
alignItems: this.props.item.pendingActions !== 0?"flex-start":undefined
For some reason, this is required to make the spinner show up when the state of the device changes. It seems that in order to make the spinner show up on a state update is to make the style of the parent view depend on the state. It doesn't seem to matter which property changes, but it needs to change in some way in order for the spinner to be affected by the update.
Does anyone have an idea about what's going on here? I'm still fairly new to Redux and React Native so there's a chance I am misusing something here but this just seems odd to me.

When updating props the component isn't self-aware about props changes.
When you update the parent state, and the props has been changed, it will trigger a render at the parent, that will also re-render the child you are talking about.
When updating a prop value, without triggering a render, the child will not re-render.
To make it aware about props changes you have to use componentDidUpdate.
An example would be:
componentDidUpdate=(prevProps)=>{
if (prevProps!==this.props)
this.setState({propsChanged:!this.state.propsChanged})
}
To use the example above, you have to define a state variable called propsChanged.
Hope this helps you understand the problem and find a solution that suits you!

Related

Simplified style change onPress React Native

The following is a first attempt at learning to simply change the style of an element onPress in react native. Being well versed in web languages I am finding it difficult as it is not as straight forward.
For reasons as yet unknown, the element requires two clicks in order to execute.
export class NavTabItem extends React.Component {
constructor(props) {
super(props);
this.state = {
active: false
}
this.NavTabAction = this.NavTabAction.bind(this)
}
NavTabAction = (elem) => {
elem.setState({active: !elem.state.active})
}
render() {
return (
<TouchableOpacity
style={this.state.active ? styles.NavTabItemSelected : styles.NavTabItem}
onPress={()=> {
this.NavTabAction(this)
}}>
<View style={styles.NavTabIcon} />
<Text style={styles.NavTabLabel}>{this.props.children}</Text>
</TouchableOpacity>
);
}
}
Other issues:
I also have not worked out how a means of setting the active state to false for other elements under the parent on click.
Additionally, Is there a simple way to affect the style of child elements like with the web. At the moment I cannot see a means of a parent style affecting a child element through selectors like you can with CSS
eg. a stylesheet that read NavTabItemSelected Text :{ // active style for <Text> }
Instead of calling elem.setState or elem.state, it should be this.setState and elem.state.
NavTabAction = (elem) => {
this.setState(prev => ({...prev, active: !prev.active}))
}
And instead of passing this in the onPress, you should just pass the function's reference.
onPress={this.NavTabAction}>
You should also remove this line because you are using arrow function
// no need to bind when using arrow functions
this.NavTabAction = this.NavTabAction.bind(this)
Additionally, Is there a simple way to affect the style of child elements like with the web
You could check styled-component, but I think that feature don't exists yet for react native. What you should do is pass props down to child components.
Thanks to everyone for their help with this and sorting out some other bits and pieces with the code.
The issue in question however was that the style was changing on the second click. A few hours later and I have a cause and a solution for anyone suffering from this. Should any of the far more experienced people who have answered this question believe this answer is incorrect or they have a better one, please post it but for now here is the only way I have found to fix it.
The cause:
Using setState was correctly re rendering the variables. This could both be seen in the console via console.log() and directly outputted in the render making them visible.
However, no matter what was tried, this did not update the style. Whether it was a style name from the Stylesheet or inline styles, they would update on the second click rather than the first but still to the parameters of the first. So if the first click should make a button turn from red to green, it would not do so even though the new state had rendered. However if a subsequent click should have turned the button back to red then the button would now go green (like it should have for the first click). It would then go red on the third click seemingly always one step behind the status passed to it.
Solution
To fix this, take the style off the the primary element (forgive terminology, someone edit), in my case, the TouchableOpacity element. Add in a child View element and place the styles on that View element instead along with the ternary operator and wallah.
It seems any change to status on the effective master element or container if you prefer, only takes affect after another render, not that contained in setStatus.
Final code:
export class NavTabItem extends React.Component {
constructor(props) {
super(props);
this.state = {
active: false
}
}
NavTabAction = () => {
this.setState({active: !this.state.active})
}
render() {
this.state.active == true ? console.log("selected") : console.log("unselected")
return (
<TouchableOpacity onPress={this.NavTabAction}>
// added View containing style and ternary operator
<View style={this.state.active == true ? styles.NavTabItemSelected : styles.NavTabItem}>
<View style={styles.NavTabIcon} />
<TextCap11 style={styles.NavTabLabel}>{this.props.children}</TextCap11>
</View>
// End added view
</TouchableOpacity>
);
}
}

React-native: how to change row's content or style while swiping?

I want to have a functionality similar to Google Inbox (demo), when swiping an item causes a change of the background color and size of the icon.
My implementation has a ListView of Swipeable components as rows and I want to change the content or style of a row (in the right pane for example) based on the swipe position.
In order to have the ListView to re-render while swiping I added a pos state which I set using the onPanAnimatedValueRef prop of the Swipeable.
The problem is that the ListView doesn't re-render and nothing changes.
<ListView
...
renderRow={(data) => (
<Swipeable rightContent={
<View><Text>{this.state.pos}</Text></View>
}
...
onPanAnimatedValueRef={(pan) => {
pan.addListener(val => {
this.setState({
pos: val.x
});
}}>
{data}
</Swipeable>
)}
/>
Do you see any problem with this?
I would wrap the Swipeable in your own component, hold state there and your animation logic. It is not the ListView's responsibility to rerender the whole thing since the data hasn't changed, just your row itself.
Alternatively, you could change the data via setState to the ListView's data source (which presumably was based in state and hence force a rerender but this is a less efficient path)

State Management with Multiple Inputs (Lists)

I'm trying to figure out the best place to manage the state for lists of input in react-native but haven't found any good, thorough examples or clear guidance and I can see a few options.
For simplicity not including specifics about the tool for managing state, as my understanding is how the state is stored doesn't impact the component where it's managed.
Scenario
A screen component that receives an array of items as props to be displayed in a List of ListItems. Each ListItem includes a input, for simplicity imagine a switch (boolean).
Use cases include an array of form questions or settings to be displayed in a list and allowing user input. Pseudocode:
class SettingsView extends Component {
render () {
return (
<View>
<List style={styles.list}>
{this.props.inputArray.map((item, index) => (
<ListItem
title={item.title}
isSwitched={item.value}
key={index}
onSwitchChange = {this.onChange}
/>
))}
</List>
</View>
);
}
}
Option 1
Based on the Thinking in React page, one option that comes to mind is managing state at the Screen (SettingsView) level by creating an array of (inputArray).length in the SettingsView constructor state and have the onChange function update that array based on key.
Option 2
The second option I see is having each ListItem manage the state it's displaying. This makes sense from an encapsulation perspective to me, but then less so for managing of the state, given that the onSwitchChange function is in the SettingsView and I'm not as clear how this would work.
Are there other options I'm not considering? Admit that experience in React/RN is limited and def coming from a more object mindset like iOS's list datasource model.
Note: Another option is having the entire inputArray in state, instead of passed as props. My understanding is that state should be minimized, so was designing that only the inputs to each item in inputArray should be in the state. i.e. Form Labels (i.e. questions) are props not state.
Option 1 would be the better choice, there is this concept "Smart Components and Dumb Components"
Smart Components: typically holds the state of all the child components associated with it, it also defines the functions that is passed down to child components to modify its state.
Dumb Components: These are components that receives props which includes data and functions they typically don't have their own state and relies heavily on the parent component.
The problem is that when you create a component you need to decide whether it's smart or dumb, usually I associate a screen to a smart component, in your example it would be the SettingsView(smart) that will hold the state and function and it's children will be the dumb components but this is really application specific decision because you might have a SettingsView that are dynamic based on context and so it would be much better to make it a dumb component let's use your example above
Since Settings View relies on this.props.inputArray passed from a parent
component(I will call this ParentComponent) you couldn't modify
inputArray directly in SettingsView what you could do is pass another prop from
ParentComponent to SettingsView which is a function that modifies inputArray
class ParentComponent extends Component {
constructor(props) {
super(props);
this.state = {
inputArray: [],
};
this.onSwitchChange = this.onSwitchChange.bind(this); // when passing fn
}
onSwitchChange(index) { // index will come from the child component
// do some logic here based on the index then update the state
this.setState('inputArray' updatedState); // updatedState is just an example variable
}
render() {
return (
<View>
<SettingsView
onSwitchChange={(index) => () => this.onSwitchChange(index)}
inputArray={this.state.inputArray}
/>
</View>
);
}
/*
(index) => () => this.onSwitchChange(index)
this is a function that will return another function
we need this because we want to delay the execution of onSwitchChange
and capture index and associate it with that method, typically this
is used if were passing function to child component(SettingsView) which will be used as
a handler for events.
*/
}
class SettingsView extends Component {
render () {
return (
<View>
<List style={styles.list}>
{this.props.inputArray.map((item, index) => (
<ListItem
title={item.title}
isSwitched={item.value}
key={index}
onSwitchChange={this.props.onSwitchChange}
/>
))}
</List>
</View>
);
}
}
This example might be pointless because you could just use SettingsView as the parent of ListItem and other components but since SettingsView state is now managed by ParentComponent it is now a dumb component and can be used in other screens that have the specific state that SettingsView needs to operate. the general goal is to create as many dumb components as possible the reason being is that these type of components are highly reusable because you just need to pass them props to properly work.

TouchableHighlight and TouchableOpacity get highlighted on render()

I experience a behaviour where TouchableHighlight and TouchableOpacity reacts visually upon render (onPress is not being called).
One thing is that it looks just a little strange, when I enter the page and my button make a small "blink". This is strange but tolerable. The more frustrating part is that if I alter state for the parent component and thus invoke a re-render(), the button will "blink" again, making all buttons blink whenever I alter state.
Pushing the buttons alters page state, and thus pushing a button makes both buttons "blink".
I use react-redux, but this should not affect this behaviour.
The code below is just for illustration.
render()
{
return(
<View>
<ToucableHightlight> //Click here changes state
<Content/>
</ToucableHightlight>
<ToucableHightlight> //Click here changes state
<Content/>
</ToucableHightlight>
<View>
);
}
Add activeOpacity in TouchableOpacity and it will force to not blink.
<TouchableOpacity style={styles.opecity} activeOpacity={1}>
I solved the problem. Earlier during my render function i defined the "Content"-components, resulting in new (but alike) components being defined during each update. Placing the definitions of "Content" outside of the render function fixed it, so that the components no longer flashes when the page is re-rendered.
This explains why my component was rendered as a new component upon each render in the parent component, but it does not explain why a TouchableHighlight blinks during its initial render.
Buttons blinking during initial render is acceptable to me - buttons blinking upon any state-change is not.
So I am sufficiently happy now.
Not sure if it's because I'm running a later version, but I found this blinking behavior happens only on the first click.
My solution was putting the code that triggers rerendering in a setTimeout
<TouchableOpacity
onPress={function() {
setTimeout(function() {
_this.setState({myState: 'someValue'})
});
}}
>

Trying to remove a view index above child count error

What does this mean?
This happens when I update a list of iterated views something like
<View style={{ flexDirection: 'row', padding: 20, backgroundColor: '#fff' }}>
<Ionicons name={jobIcon} color={theme.iconColor} size={30} />
<Text>{jobService}</Text>
<Text>{jobDate}</Text>
</View>
mapped inside a scrollview.
this error pops up when I modify the array from child scene.
scene1 - is where the ScrollView with job list array of views
sence2 - is where I delete a job and should update scene1 when I do remove a job
If you want the LayoutAnimation to work with ScrollView, replace
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
with
LayoutAnimation.configureNext({
duration: 300,
create:
{
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
update:
{
type: LayoutAnimation.Types.easeInEaseOut,
}
});
This works on both Android and iOS without any crashes.
In my case I was using LayoutAnimation for my ScrollView. Inside it a map of Items. When an Item is removed from the list this happens. Not using LayoutAnimation seems to be working fine.
It's happening when you call layout animation when it already in process. iOS will show an warning while Android will explode with this error.
You can use this easy pattern to fix it when you're using LayoutAnimation from same component.
layoutAnimation() {
if (!this.layoutAnimationActive) {
this.layoutAnimationActive = true;
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInOut, () => { this.layoutAnimationActive = false; });
}
}
This happens when a component only has x amount of children, but you are trying to remove a child with index greater than x. Like an index out of bounds exception that is common with arrays. Can cause lots of frustration because often times the child you are trying to remove DOES INFACT EXIST. But might be happening because you are using a third party component that only expects a certain amount of children.
For me, it happened when I put an extra child into air-bnb MapView. I fixed the issue by making this element the child of it's grandparent (it was absolutely positioned so it didn't affect styling).
I encountered this issue when creating a react-native application using Expo.
This error was often getting raised when I was rendering new Markers onto a Map (which was rendered via a MapView component).
What fixed the issue for me was to add a key prop to the Marker component e.g.
<Marker coordinate={coordinate} key={`${coordinate.latitude}_${coordinate.longitude}`} />
Hope this helps anyone who also encounters this issue when dealing with MapView and Marker components!
In my case, I was deleting an item in a virtualized list. I was using redux to manage the data of the list and updated the list with the new data. However, the component for the item that was deleted caused this error.
I solved this by adding a boolean state property on this item component, example "hideItem". My item was a class component but a setState hook could also be used for a functional component. I set this to true after deletion. The post hid with layout animation correctly, and when the page refreshed it no longer rendered the deleted item. Hence, the error went away on android without needing to not use LayoutAnimation.
Evidently, it seems this bug can show its ugly face in various conditions. After a couple of hours of debugging, I found my root cause to be a key prop being passed to a component that didn't require it. I'm still unsure of why this was causing a crash, however, I suspect it has to do with the fact that the component accepts the key as a unique way to identify the view, but was also using the componentWillReceiveProps(props: *) lifecycle method that updated the component's state.
This is likely to happen if you're using some native components, where some ViewManager returns a LayoutShadowNode in createShadowNodeInstance of ViewGroupManager or something extending ReactShadowNode in createShadowNodeInstance of ViewManager on Android, and a RCTShadowView in the shadowView method of RCTViewManager on iOS. But, returns null/nil for some other View in some other ViewManager.
Then, if you combine children of both types in the same parent, and any of the elements without shadowViews/Nodes come before the changing number of elements which do have shadowViews/Nodes, then the indices won't match up, and the RCTUIManager on iOS and NativeViewHierarchyManager on Android will choke and produce these exceptions.
I solved a similar issue in react-native-svg recently, by making all the ViewManagers return values rather than null/nil. https://github.com/facebook/react-native/issues/23350
So, try upgrading react-native-svg to v9.2.4 and the issue might be fixed. Or, try moving the IonIcons to the end of your children.
I had the same issue, because I had an Array.map inside a condition which I was updating using LayoutAnimation.
I fixed it by moving the state variable inside the map function.
I am using vitalized flatlist with swipe left right delete option, and when list is more than 200 items and I use LayoutAnimation then the above problem comes out on arbitrary based, I figured out that deleted item still exist because of LayoutAnimation so you can play around with timings of update using the following code
LayoutAnimation.configureNext({
duration: 300,
create:
{
duration:500
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
update:
{
duration: 500,
type: LayoutAnimation.Types.easeInEaseOut,
}
});
this.refs['task'].setNativeProps({
style: {transform: [{translateX: Dimensions.get('window').width}]},
});
one more thing, I am also hiding the item suddenly after it's deletion occurs
here is code snippet
this.props.handleDeleteTask(this.props.item.id);
this.setState({hideItem:true}) // here I am hiding through state function, so the item should disappear right after it's deletion.