"Maximum update depth exceeded ..." when parent callback is called in React-Native - react-native

I still don't get why I'm getting an update depth exceeded on this specific case, i would appreciate if any help is given.
To give some insight.
I got a Parent and a Child (multi-list selector) component that I created.
This multi-list allows you to select multiple items and when modal is closed, I execute a callback that comes from Parent, with the list of items selected and update it from the Parent itself by doing setState.
On Parent's constructor I got the callback function like this:
Parent
this.onItemSelected = this.onItemSelected.bind(this);
This is the function above:
onItemSelected(updatedList) {
this.setState({selectedItemList: updatedList});
}
This is when I use it and pass it to Children.
renderModalMultiSelect = () => {
return (
<MultiSelect
items={this.state.itemList}
onItemSelect={ this.onItemSelected }
disabled={this.state.filters.type === 'all'}
isLoadingList={this.state.isLoadingList}
/>
)
}
Of course this is on Parent's render() function as { this.renderModalMultiSelect() }
Children
On this component, I handle the selected item list locally, is not on the State, so whenever I press on items, I just simply do a push on this local array.
When I close the modal, I run the parent props callback.
onClose = () => {
this.setState({ modalOpened: false }, () => {
// Optional callback
if (this.props.onItemSelect) {
this.props.onItemSelect(this.selectedItems);
}
})
}
Again, this function is used on children's render()
<Button onPress={ this.onClose } transparent>
<Icon name="md-arrow-back" style={ styles.headerIcon }/>
</Button>
Before this, I was passing a state from Parent to Children with the list of selected items, but it didn't seem right to do things like: this.props.selectedItemList.push(item) or is this fine to do?
Thanks and I hope I can understand what's going on here, it keeps throwing this error whenever I close the modal (onClose is called).

Related

Lifting state up does not work in React native

I probably misunderstood something very basic here.
I want to run a method in my child component whenever the state "oranges" in my parent component changes.
My Child component looks like this:
const [someText, setSomeText] = useState(props.oranges);
const consoleThis= () => {
if (someText == true){
console.log("win!")
}else{
return
}
};
useEffect(() => {
consoleThis();
}, [someText]);
This is the code in my parent component
...
const [apples, setApples] = useState(false);
<child
oranges ={apples}
/>
...
<Pressable
onPress={() => {
setApples(true);
}
>
Any ideas why I don't get my "win!" console logged?
useState(initial) doesn't reset the state each time the value passed to it changes - it is only used for setting the initial state of the value. Passing a new prop value will not update the state variable value - the state value becomes its own copy of the initial value that is managed independently of the prop value. In general, when you're lifting the state up, you don't want to also be keeping state in your child component. Instead, just drive the child component off of the state value passed via props.
You probably want something that responds to changes in the orange prop, like this:
function ChildComponent({orange}) {
useEffect(() => {
if (orange) {
console.log("win!")
}
}, [orange])
};

React Native doesn't re-render on DOM change

I had an array of components inside a ScrollView component. Somehow react native doesn't re-render when the array is modified.
Here's a demonstration of my problem:
const TestApp = () => {
const [arr, setArr] = useState([]);
function pushArr() {
setArr((arr) => {
arr.push(1);
return arr;
});
console.log('pushArr():', arr);
}
function flushArr() {
setArr([]);
console.log('flushArr():', arr);
}
useEffect(() => {
console.log('useEffect():' , arr);
})
return (
<>
<ScrollView style={{flex:1}}>
{arr.map((elem, i) => <Text key={i}>{elem}</Text>)}
</ScrollView>
<Button title="Push" onPress={pushArr}></Button>
<Button title="Flush" onPress={flushArr}></Button>
</>
)
}
The page remains blank, and no updates happen on button press.
I've logged out arr and these are my findings:
pushArr() and flushArr() works as expected
useEffect() gets triggered only on startup and after flushArr()
Can anyone explain this behavior, and what mistakes have I made?
If I remember correctly, you need to make a copy of the array whenever you want it to “react”. The new memory address will let react know it should update. In other words, you shouldn’t mutate the array.
You can use the spread operator to make a copy and then push an element to the end which you can then pass to useArr. Usually I see people just passing the new object inside your useArr function.
I also don’t see you passing anything to your useArr function.

How do I call functions inside render() method and still keep it pure?

I want to use a modal in my React Native app that ask the user to confirm his action.
The state looks like this:
state = {
dialogVisible: false,
confirmed: null
}
If the user confirms his delete action (turning confirmed to true), I want to execute my delete() method.
Delete method:
delete = () => {
const { deckName } = this.props.navigation.state.params
console.log('WAS CONFIRMED')
this.setState({
dialogVisible: false
})
this.props.navigation.navigate('Decks')
removeDeckFromStorage(deckName)
this.props.dispatch(removeDeck(deckName))
this.setState({
confirmed: null
})
}
noDelete = () => {
this.setState({
dialogVisible: false
})
this.setState({
confirmed: null
})
}
When the user confirmed his action, the modal closes, and the delete is done. Afterwards, I want to set confirmed back to null re-use it later.
On the other hand, if the user does not confirm the modal by clicking No, the noDelete() method should be called, which just closes the modal and sets confirmed back to null.
My problem is now that I get a warning saying:
Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
That is because I check for the state of confirmed inside the render() method:
const { confirmed } = this.state
if (confirmed){
this.delete()
}
else if (confirmed === false){
this.noDelete()
}
I did this because when I checked the state of confirmed inside the delete method right after setting the confirmed state to true, it always said null.
I put the check inside render because after the confirmed state is changed through the user input, the component is re-rendered and thus giving me the right state for the query.
The dialog buttons each change the current confirmed state when clicked:
positiveButton={{
title: "YES",
onPress: () => this.setState({confirmed: true})
}}
So, how can I check for confirmed after it was set but still outside of the render method to keep it pure?
You should never update the state in the render method. You should move your logic to the delete and noDelete functions instead.
I'm not sure how your modal is, however let's suppose it's something like this:
<View>
<TouchableOpacity onPress={this.delete}>
<Text>Delete</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.noDelete}>
<Text>No Delete</Text>
</TouchableOpacity>
</View>
And in the delete and noDelete you simply remove the setState({ confirmed }) since you're already calling the deletion from there.

Using FlatList#onViewableItemsChanged to call a Component function

I'm currently attempting to implement a form of LazyLoading using the FlatList component, which introduces a neat little feature called onViewableItemsChanged which gives you a list of all of the components that are no longer on the screen as well as items that are now on the screen.
This is a custom LazyLoad implementation and as such is more complicated than most LazyLoad open-sourced libraries that are available, which is why I'm working on my own implementation. I'm already looked into react-native-lazy-load and others.
Basically, I need to be able to call a function that's part of the component being rendered in the FlatList, I've tried creating a reference to the item rendered in the FlatList and calling it as such, but it doesn't seem to work.
For example:
<FlatList data={...}
renderItem={(item) => <Example ref={(ref) => this[`swiperRef_${item.key}`] = ref}}
onViewableItemsChanged={this.onViewableItemsChanged}
/>
onViewableItemsChanged = ({viewableItems}) => {
viewableItems.forEach((item) => {
const { isViewable, key } = item;
if(isViewable && !this.cachedKeys.includes(key)) {
const ref = this[`swiperRef_${key}`];
if(!ref) return console.error('Ref not found');
ref.startLoading();
this.cachedKeys.push(key);
}
});
}
Now in the <Example /> component I would have a function called startLoading which should be called when a new visible item is brought onto the screen, however the ref never exists.
I was actually doing everything correctly, but I accidently forgot to deconstruct the parameter returned from the renderItem function, so (item) should have been ({ item })
That's all there was to it.

React-Native how to update dropdown

I'm using react-native-chooser to create my dropdowns and when I select 1 item from dropddown1 I want to update the items from dropdown2. Thanks.
In react, to make your UI change, you need to update your state. From looking at the docs for react-native-chooser it has a callback method called onSelect. Here, the currently selected option is returned to you to use. Based on this selected option you can update the state of second dropdown. The important part here is the parent child relationship. In react, a child is only re-rendered if the parent's state is updated (unless otherwise specified). Some pseudocode:
// Your method callback
onSelect = (option) => {
const newOptions = computeNewOptions(option)
this.setState({options: newOptions})
}
// Your Second dropdown component would take these options in as a prop
render () {
return (
<SecondDropDown options={this.state.options} />
)
}
// You can then access your options through the props
export default class SecondDropDown extends React.Component {
render () {
let myOptions = renderOptions(this.props.options)
return (
<View>
{myOptions}
</View>
)
}
}