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

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
}}

Related

How to create an rxjs Observable from TextInput (either onChange or onTextChange)

I want to create an observable from a change event that gets fired on a React Native TextInput component. TextInput comes with 2 change props that I'm aware of (onChangeText and onChange). From what I gather, you need to use onChange if you want access to the native event you need to use onChange.
I don't know much about the native event object. I am trying to create an rxjs observable using fromEvent.
First I created a ref in my functional component like this:
const sqftRef = useRef().current
Then I attached this ref to the TextInput component like this:
<TextInput
ref={sqftRef} // attach a ref
label='Sqft'
mode='flat'
textContentType='none'
autoCapitalize='none'
keyboardType='numeric'
autoCorrect={false}
value={String(formValues.sqft)}
dense
underlineColor={colors.colorOffWhite}
onChangeText={(text) => setText(text)}
onChange={e => {
// somehow create an observable from this event ???
}}
style={styles.inputStyles}
theme={inputTheme}
/>
I tried to create an Observable using fromEvent like this but it doesn't work. I get undefined is not an object (evaluating target.addEventListener):
fromEvent(sqftRef, 'onChange').subscribe(value => console.log(value))
I know my approach is all wrong. Hoping someone can point me in the correct direction.
I would emit events you need into a subject, then subscribe to the subject in other parts of your code.
Here's a simple React example that should get you started
function App() {
const textChange = new Subject<string>();
useEffect(() => {
// subscribe to
const subscription = textChange.asObservable().subscribe(console.log)
return () => subscription.unsubscribe()
}, [])
// Emit events with a subject
return <textarea onChange={(e) => {
textChange.next(e.target.value)
}}>
</textarea>
}
render(<App />, document.getElementById('root'));
Check out the example here: https://stackblitz.com/edit/react-ts-akoyfv
I think the problem is with assigning the current directly to the sqftRef. Try to define it without current, but use current when creating the Observable, like the following:
const sqftRef = useRef();
Then create the Observable within useEffect to make sure that the DOM is ready:
useEffect(() => {
fromEvent(sqftRef.current, 'onChange').subscribe((value) =>
console.log(value)
);
});
OK, I was able to figure it out with the help of Amer Yousuf and Alex Fallenstedt.
I did something similar to what Alex suggested, modifying his solution for React Native. One reason his solution wasn't working for me is that it is important to use the useRef hook to prevent the Observable from being re-created on each render. If the observable is recreated (on a re-render) and useEffect doesn't run again, then we won't have an active subscription to the newly (re-created) observable (useEffect never runs again). That's why my call to sqft$.next was originally only being called once (the first time until we re-render).
My solution looks like this:
let sqft$ = useRef(new BehaviorSubject(0)).current
useEffect(() => {
const sub = sqft$.subscribe({
next: (val) => {
// just testing stuff out here
updateForm('sqft', val)
updateForm('lot', val * 2)
}
})
// this is only relevant to my use case
if (activeReport) sqft$.next(activeReport.sqft)
return () => sub.unsubscribe()
}, [activeReport])
and of course I call this in onChangeText:
onChangeText={(text) => {
sqft$.next(text)
}}
So this is working right now. I still feel like there may be a better way using onChange(e => ...stuff). I will leave this question open for a little bit in case anyone can break down how to do this using nativeEvent or explain to me how I can access an event off the TextInput component.

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.

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

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).

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.