Using useState in useEffect - react-native

I'm using useState to set text from TextInput, and I'm using a useEffect to overwrite the behavior from the back button.
const [text, setText] = useState("");
<TextInput
onChangeText={text => setText(text)}
defaultValue={text}
/>
const printVal() {
console.log("text is " + text);
}
useEffect(() => {
navigation.setOptions({
headerLeft: () => (
<HeaderBackButton onPress={() => printVal()} />
)
});
});
This always results in the text logged being the initial value of useState. If I don't use useEffect it works, but I don't want to set navigation option with every change.
Can I get the current value from useState in my useEffect, or is another solution needed?

You forgot to add dependencies as 2nd parameter to useEffect. you can add an empty array if you want your function to be called only 1 time when component loads, or for this example you should add text as dependecy because useEffect depends on this value
useEffect(() => {
navigation.setOptions({
headerLeft: () => (
<HeaderBackButton onPress={() => printVal()} />
)
});
}, [text]); //<- empty array here

Related

How to reduce the number of requests in React Navigation TopTabNavigator using expo?

i'm pretty new on React Native and currently i am developing an app using expo. I am using TopTabNavigator from react navigation 6 and i don't understand how to reduce the no of the requests. Basically, whenever i hit a certain tab the request is made. (because the component is recreated - even if i come back on previous tab which is the same, no data modified). I tried to use useFocusEffect from react navigation but it does not work as i expected. Maybe i should make the requests in the ProfileTabsScreen and pass the data via props to the particular tabs?
MAIN COMPONENT
const ProfileStatsScreen = (props) => {
const { userId } = props.route.params;
const { initialRoute, username } = props.route.params;
const RatingsDetails = () => <RatingsTab userId={userId} />;
const FollowersDetails = () => <FollowersTab userId={userId} />;
const FollowingsDetails = () => <FollowingsTab userId={userId} />;
return (
<SafeAreaView style={styles.screen}>
<Header title={username} />
<TopTabContainer initialRouteName={initialRoute}>
<Tab.Screen
name="Ratings"
component={RatingsDetails}
options={{ tabBarLabel: "Reviews" }}
/>
<Tab.Screen
name="Followers"
component={FollowersDetails}
options={{ tabBarLabel: "Followers" }}
/>
<Tab.Screen
name="Following"
component={FollowingsDetails}
options={{ tabBarLabel: "Followings" }}
/>
</TopTabContainer>
</SafeAreaView>
);
};
TAB COMPONENT (RATINGS)
export const RatingsTab = ({ userId }) => {
const { user } = useAuth();
const [reviews, setReviews] = useState([]);
const [loading, setLoading] = useState(false);
useFocusEffect(
React.useCallback(() => {
setLoading(true);
axios
.get(`${process.env.BASE_ENDPOINT}/users/${userId}/reviews`, {
headers: { Authorization: `Bearer ${user?.token}` },
})
.then((res) => {
setReviews(res.data.reviews);
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
setLoading(false);
}, [userId, user?.token])
);
const renderRatings = ({ item }) => {
const { reviewer, rating, review, createdAt } = item;
return (
<CardRatings
avatar={reviewer?.avatar}
name={reviewer?.name}
date={moment(createdAt).format("LL")}
rating={rating}
review={review}
service={"Tuns"}
/>
);
};
return (
<>
{!loading && (
<FlatList
data={reviews}
keyExtractor={(item) => item?._id}
renderItem={renderRatings}
/>
)}
{loading && <Spinner />}
</>
);
};
You're very close to the solution, your useFocusEffect is configured properly. Change the lines
useFocusEffect(
React.useCallback(() => {
setLoading(true);
to read
useFocusEffect(
React.useCallback(() => {
if (isLoading) return;
setLoading(true);
i.e., if loading is true, don't make the axios call. While this doesn't eliminate the possibility of extra requests, it should reduce what you're seeing by quite a bit.
Also, since you're using .then, wrap the final line of your callback in .finally.
.finally(()=> {
setLoading(false)
});
Otherwise, your loading state will be set to false before the promise resolves.
Thank you, but unfortunately is not working. Let's assume that i am already in the RatingsTab and i have the data because by far the request was already made. If i go to FollowersTab and after that i come back to RatingsTab i don't want to make call if the data has not changed. if (isLoading) return; i think it will not helping me, because the loading state is false at first (when the Ratings Tab component is re-created).

Changing FlatList renderItem prop via onViewableItemsChanged

I am trying to pass dynamic value to property visible to each item of FlatList if they are inside the viewport, thus value of this prop changes on scroll, but I am getting a bit laggy scrolling experience (items are changing positions on every rerender, something like jumping up and down).
How can I avoid it?
my code looks like
import React, { useState, useRef } from 'react';
import {FlatList} from 'react-native';
const TabOne = ({ data, ...rest }) => {
const [visibleIdx, setVisibleIdx] = useState(null);
const onViewableItemsChanged = useRef(({ changed }) => {
setVisibleIdx(changed.length ? changed[0].item.id : null);
});
const renderItem = (item) => (
<ContentItem isVisible={item.id === visibleIdx} />
);
return (
<FlatList
data={data}
viewabilityConfig={{ viewAreaCoveragePercentThreshold: 75 }}
onViewableItemsChanged={onViewableItemsChanged.current}
renderItem={({ item, index }) => renderItem(item, index)}
{...rest}
/>
);
};
export default TabOne;

showing custom component in header

On one screen, I would like to have a custom component to be shown on the header.
I tried:
const MyScreen = ({route, navigation}) => {
navigation.setOptions({
headerTitle: props => {
<MyComponent {...props} />;
},
});
...
export default MyScreen;
But it shows blank like below:
To further verify the approach is correct, I also tried to simply show a text:
navigation.setOptions({
headerTitle: props => {
<Text {...props}> Hello </Text>;
},
});
It also shows blank in the header title area. What am I missing? How to show a custom component in the header?
(MyScreen is hosted by a stack navigator)
The issue is that you are not returning the component, hence why it is not displayed.
You should either write:
const MyScreen = ({route, navigation}) => {
React.useLayoutEffect(() => {
navigation.setOptions({
headerTitle: props => {
return <MyComponent {...props} />;
},
});
}, [navigation]);
...
export default MyScreen;
or for a more concise syntax:
const MyScreen = ({route, navigation}) => {
React.useLayoutEffect(() => {
navigation.setOptions({
headerTitle: props => <MyComponent {...props} />,
});
}, [navigation]);
...
export default MyScreen;
You also need to use useLayoutEffect because you cannot update a component from inside the function body of a different component.
useLayoutEffect runs synchronously after a render but before the screen is visually updated, which will prevent your screen to flicker when your title is displayed.
By the way, since you seemingly don't need to use a function as props of your component, you could define this option directly in your navigator like so:
<Stack.Navigator>
<Stack.Screen
name="MyScreen"
component={MyScreen}
options={{ headerTitle: props => <MyComponent {...props} /> }}
/>
</Stack.Navigator>

Access a function which is defined inside a functional component in navigationOptions

I need to access a function which uses state values. Following is a sample code of my current implementation.
import React, { useState, useEffect } from 'react';
import { View, Text, Button, TouchableOpacity } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { withNavigationFocus } from 'react-navigation';
const HomeScreen = ({ navigation }) => {
const [name, setName] = useState('');
useEffect(() => {
navigation.setParams({
onSave
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onSave]);
const onSave = () => {
// name value will be used in this function
console.log(name);
};
return (
<View>
<Text>{name}</Text>
<Button title="Change name" onPress={() => setName('John')} />
</View>
);
};
HomeScreen.navigationOptions = ({ navigation }) => {
const onSave = navigation.getParam('onSave', false);
return {
title: 'Home',
headerRight: (
<TouchableOpacity onPress={onSave}>
<MaterialCommunityIcons name="content-save" color={'black'} />
</TouchableOpacity>
)
};
};
export default withNavigationFocus(HomeScreen);
Even-though I'm able to access the onSave function. I'm not able to get updated 'name' state. I’m aware that we can reset onSave param on state change, but if there many states needs to be accessed inside onSave function what is the best way to handle this situation?
What if you change the useEffect dependency to 'name' variable:
useEffect(() => {
navigation.setParams({
onSave
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [name]);
Does it make a difference?

React Navigation 5 headerRight button function called doesn't get updated states

In the following simplified example, a user updates the label state using the TextInput and then clicks the 'Save' button in the header. In the submit function, when the label state is requested it returns the original value '' rather than the updated value.
What changes need to be made to the navigation headerRight button to fix this issue?
Note: When the Save button is in the render view, everything works as expected, just not when it's in the header.
import React, {useState, useLayoutEffect} from 'react';
import { TouchableWithoutFeedback, View, Text, TextInput } from 'react-native';
export default function EditScreen({navigation}){
const [label, setLabel] = useState('');
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={submit}>
<Text>Save</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation]);
const submit = () => {
//label doesn't return the updated state here
const data = {label: label}
fetch(....)
}
return(
<View>
<TextInput onChangeText={(text) => setLabel(text) } value={label} />
</View>
)
}
Label should be passed as a dependency for the useLayouteffect, Which will make the hook run on changes
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={submit}>
<Text>Save</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation,label]);
Guruparan's answer is correct for the question, although I wanted to make the solution more usable for screens with many TextInputs.
To achieve that, I added an additional state called saving, which is set to true when Done is clicked. This triggers the useEffect hook to be called and therefore the submit.
export default function EditScreen({navigation}){
const [label, setLabel] = useState('');
const [saving, setSaving] = useState(false);
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableWithoutFeedback onPress={() => setSaving(true)}>
<Text>Done</Text>
</TouchableWithoutFeedback>
),
});
}, [navigation]);
useEffect(() => {
// Check if saving to avoid calling submit on screen unmounting
if(saving){
submit()
}
}, [saving]);
const submit = () => {
const data = {label: label}
fetch(....)
}
return(
<View>
<TextInput onChangeText={(text) => setLabel(text) } value={label} />
</View>
)
}