React Native Context dispatch delete current screen(item) causes render error - react-native

In my app, I have a list view of recipes and also a detail view of just one recipe.
I would like, in the detail view, to delete the recipe.
export default function RecipeDetails({ route, navigation }) {
const { recipes, dispatch } = useContext(RecipeContext);
const id = route.params.item.id;
const currentRecipe = recipes.find((r) => r.id === id);
return (
<ScrollView>
<TouchableOpacity
onPress={() => {
dispatch({ type: "remove", obj: currentRecipe });
navigation.goBack();
}}
>
<FontAwesomeIcon icon={faTrash} size={20} />
</TouchableOpacity>
<Image
style={styles.img}
source={images[currentRecipe.image]}
resizeMode="cover"
/>
My goal is to dispatch the remove action and then go back to the list view.
However, right now, after the dispatch of remove fires, I get an error saying "currentRecipe.image" is undefined.
My question is, why doesn't the navigation direct back to the last screen(list view) so this undefined error won't occur ?

This happens because your component wants to re-render after the recipe is removed but before you navigate back -- it receives new props where the recipe is already gone and so currentRecipe is undefined and accessing currentRecipe.image throws an error.
What you can do is prevent accessing currentRecipe if it does not exist.
One way to do this is by returning null or a placeholder - this is not the most elegant solution:
const currentRecipe = recipes.find((r) => r.id === id);
if (!currentRecipe) return null;
Alternatively, you can copy your recipe from context to local state for reading:
const [currentRecipe] = useState(recipes.find((r) => r.id === id));
This will mean currentRecipe does not change after you have initially loaded it - not when you remove it, but also not when user edits it (if they have that option).

Related

What is the best practice in React Navigation to update a list of items after the item is altered?

Here is the scenario :
I have a bottom tab with two tabs : Actions and People
One Action always belong to one Person
On the People tab, I can view, add and edit an Action to do
If I am in the stack People > Person from where I edit an action,
How can I update only the Action row in the Actions list ? What is the best practice ?
The best would be to not use redux for this if possible, in order to make a good use of react navigation's redux...
Tell me if it's not clear enough
It might help to see some example code, but generally speaking you want to move the information from the "Person" screen all the way up to a common parent and then back down to a child (the Action screen/tab).
Here's some pseudo code with only 1 action, where changing the action on a Person Screen will ultimately update that action in the Action Tab:
function Root({ }) {
const [action, setAction] = useState('initialAction')
return (
<View >
<PeopleTab setAction={setAction} action={action} />
<ActionTab setAction={setAction} action={action} />
</View>
);
}
function PeopleTab({ setAction, action }) {
return (
<View >
<PersonScreen setAction={setAction} action={action}/>
</View>
);
}
function PersonScreen({ setAction, action }) {
return (
<View >
// handle setting the action here with some onPress={() => setAction('new action'))
</View>
);
}
function ActionTab({ setAction, action }) {
return (
<View >
// Handle displaying the action here {action}
</View>
);
}

How do I get TouchableOpacity to call functions in a specifid order?

I am trying to set the state of a component prior to making the API call. The problem is the API call being called first. Here is what I have.
onPress={() => {
setMeal('dinner');
addToLogButtonPressed();
}}
When I press the button addToLogButtonPressed(); calls first which causes an error.
How to I call setMeal before addToLogButtonPressed?
I think you can use useEffect to do that
const [meal, setMeal] = useState('')
useEffect(() => {
addToLogButtonPressed();
}, [meal])
onPress={() => {
setMeal('dinner');
}}
I also face this problem for my previous project simply just pass meal value to your addToLogButtonPressed() and access it inside the function.
If you are not using "dinner" value anywhere else you can skip setting state it will save you one Re-render.
onPress={() => {
setMeal('dinner');
addToLogButtonPressed('dinner'); // like this
}}

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.

Unable to receive props to dump component

I tried all possible ways to get a simple parameter on file MembershipCard.js. My Home component Home.js simply passes props to MembershipList.js where I have done minor Array operations and iterate it to prepare a list. Each item from the list is then pass on to third file MembershipCard.js. I'm getting membership object in this file and able to prepare a card list at Home page. On Home page I have to show a side line whereas I don't want this side line on other pages (which are also accessing MembershipCard.js) hence I'm trying to send a variable on which I will conditionally show side line.
But after so many try out I'm still receiving undefined
This is my React component - Home.js
render () {
return (
<Surface>
<GreetingCard profile={this.props.profile.Profile}/>
<MembershipList props={this.props}/>
</Surface>
)
}
MembershipList.js - this contain only few functions
renderMembershipCard = (membership, i, props, sideLine = true) => {
return (
<TouchableOpacity key={i} style={styles.membership} onPress={() => props.navigation.navigate('Item', { title: membership.gym_name })}>
{/* <MembershipCard {...{membership, sideLine }}/> */}
<MembershipCard {...membership} sideLine={sideLine}/>
</TouchableOpacity>
)
}
const MembershipList = (props) => {
let membership = props.props.profile.Membership
let listArray = [];
Object.keys(membership).forEach(key => listArray.push(this.renderMembershipCard(membership[key], key, props.props)));
return (
<View>
<Text style={styles.ListTitle}>Active Membership ({listArray.length})</Text>
{listArray}
</View>
);
}
MembershipCard.js - this file is part of my presentation layer. It only return a Card design.
const MembershipCard = ({membership,sideLine}) => {
console.log('sideLine', sideLine); // showing undefined
console.log('membership', membership);
return (
<Card>
<Text style={styles.gymTitleText}>{membership.name}</Text>
... JSX code
</Card>
)
Make the following changes to your code and it should work. Change props.props seems to be incorrect way of passing props. Use spread operator for passing all props to children in correct manner.
<MembershipList {...this.props}/>
const MembershipList = (props) => {
let membership = props.profile.Membership
let listArray = [];
Object.keys(membership).forEach(key => listArray.push(this.renderMembershipCard(membership[key], key, props)));
return (
<View>
<Text style={styles.ListTitle}>Active Membership ({listArray.length})</Text>
{listArray}
</View>
)}
<MembershipCard membership={membership} sideLine={sideLine}/>
I solved it using simple trik.
Instead of calling it as a component -
<MembershipCard {...membership} sideLine={sideLine}/>
call it as a simple JS function using curly braces {} -
{ MembershipCard (membership, sideLine) }
This way I can easily pass as many parameters and can easily access all those in called function.

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.