State updating only after second onPress - reactjs

I'm building a React Native app which renders a flatlist of groups. By pressing on one group, you get to see all the individual items nested within.
I'm using the following this navigation library and I'm passing props as such:
const navigateToDetails = useCallback(() => {
Navigation.push(props.componentId, {
component: {
name: pagesNames.DETAILS,
passProps: {
groupId: selectedGroup,
},
},
});
});
All of this is within a functional component which has a couple more relevant things, to begin with we have the state and function that gets us selectedGroup:
const [selectedGroup, setSelectedGroup] = useState(Number);
function handleTouch(value) {
setSelectedGroup(value);
console.log('value: ' + value);
navigateToDetails(value);
}
value is taken from an item in a Flatlist which gets its data from this fake API.
Value is retrieved from the list in this fashion:
<FlatList
data={data}
renderItem={({ item }) => (
<TouchableOpacity
style={[styles.card, { width: windowWidth - 30 }]}
onPress={() => handleTouch(item.id)}>
{misc components}
</TouchableOpacity>
)}
/>
Expected Behaviour
I'm expecting that when I tap an item from the list, it'd navigate to the Details page and populate it with its data. However this does not happen. Upon logging out value and selectedGroup, I find that value is always logged out correctly but selectedGroup is always first logged out as 0.
Only after a second press does it show the expected id and populate the Details page.

You are redirected before state is set. initialize selectedGroup with null/undefined/0
try navigating in useEffect
function handleTouch(value) {
setSelectedGroup(value);
console.log('value: ' + value);
}
useEffect(() => {
if (selectedGroup) {
navigateToDetails(selectedGroup)
}
}, [selectedGroup, navigateToDetails])

Related

Unable to prevent Flatlist from re-rendering (already using useCallback and memo)

I'm building a to do list app as part of a coding course, using Firebase Realtime Database and React Native with Expo.
I have no problems rendering the to do list, and in this case clicking a checkbox to indicate whether the task is prioritized or not.
However, each time I click on the checkbox to change the priority of a single task in the to do list, the entire Flatlist re-renders.
Each task object is as follows:
{id: ***, text: ***, priority: ***}
Task Component: (It consists of the text of the to do (task.text), and also a checkbox to indicate whether the task is prioritized or not). I've wrapped this component in React.memo, and the only props passed down from Todolist to Task are the individual task, but it still re-renders every time. (I left out most of the standard imports in the code snippet below)
import { CheckBox } from '#rneui/themed';
const Task = ({
item,
}) => {
console.log(item)
const { user } = useContext(AuthContext);
const onPressPriority = async () => {
await update(ref(database, `users/${user}/tasks/${item.id}`), {
priority: !item.priority,
});
};
return (
<View
style={{ flexDirection: 'row', alignItems: 'center', width: '95%' }}
>
<View
style={{ width: '90%' }}
>
<Text>{item.text}</Text>
</View>
<View
style={{ width: '10%' }}
>
<CheckBox
checked={item.priority}
checkedColor="#00a152"
iconType="material-community"
checkedIcon="checkbox-marked"
uncheckedIcon={'checkbox-blank-outline'}
onPress={onPressPriority}
/>
</View>
</View>
)}
export default memo(Task, (prevProps, nextProps) => {
if (prevProps.item !== nextProps.item) {
return true
}
return false
})
To Do List parent component: Contains the Flatlist which renders a list of the Task components. It also contains a useEffect to update the tasks state based on changes to the Firebase database, and the function (memoizedOnPressPriority) to update the task.priority value when the Checkbox in the task component is clicked. Since memoizedOnPressPriority is passed a prop to , I've tried to place it in a useCallback, but is still re-rendering all items when the checkbox is clicked. (I left out most of the standard imports in the code snippet below)
export default function Home2() {
const { user } = useContext(AuthContext);
const [tasks, setTasks] = useState([]);
useEffect(() => {
if (user) {
return onValue(ref(database, `users/${user}/tasks`), (snapshot) => {
const todos = snapshot.val();
const tasksCopy = [];
for (let id in todos) {
tasksCopy.push({ ...todos[id], id: id });
}
setTasks(tasksCopy);
});
} else {
setTasks([]);
}
}, [user]);
const renderItem = ({ item }) => (
<TaskTwo
item={item}
/>
);
return (
<View style={styles.container}>
<FlatList
data={tasks}
initialNumToRender={5}
windowSize={4}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
</View>
);
}
Could anyone let me know what I'm doing wrong, and how I can prevent the entire Flatlist from re-rendering each time I invoke the memoizedOnPressPriority function passed down to the Task component from the TodoList parent component? Any help is much appreciated!
The flamegraph for the render is below:
Update: I moved the prioritize function (memoizedOnPressPriority) into the Task component and removed the useCallback - so it's not being passed as a prop anymore. The re-render still happens whenever I press it.
Update 2: I added a key extractor , and also a custom equality function into the memoized task component. Still keeps rendering!
I'm not familiar with Firebase Realtime Database, but if I understand the logic correctly, the whole tasks array is updated when one item changes, and this is what is triggering the list update.
Fixing the memo function
Wrapping the Task component in memo does not work because it performs a shallow comparison of the objects. The objects change each time the data is updated because a new tasks array with new objects is created, so the references of the objects are different.
See this post for more details.
To use memo, we have to pass a custom equality check function, that returns true if the component is the same with new props, like so:
export default memo(Task, (prevProps, nextProps) => {
if (prevProps.item.id === nextProps.item.id && prevProps.item.priority === nextProps.item.priority ) {
return true;
}
return false;
})
Note that is the text is modifiable, you'll want to check that too.
Alternative solution : read data from the Task component
This solution is recommended and takes full advantage of Firebase Realtime Database.
To update only the component that is updated, you need to pass an array of ids to your flatlist, and delegate the data reading to the child component.
It's a pattern I use with redux very often when I want to update a component without updating the whole flatlist.
I checked the documentation of Firebase Realtime Database, and they indeed encourage you to read data at the lowest level. If you have a large list with many properties, it's not performant to receive the whole list when only one item is updated. Under the hood, the front-end library manages the cache system automatically.
//TodoList parent Component
...
const [tasksIds, setTasksIds] = useState([]);
useEffect(() => {
if (user) {
return onValue(ref(database, `users/${user}/tasks`), (snapshot) => {
const todos = snapshot.val();
// Build an array of ids
const tasksIdsFromDb = todos.map((todo) => todo.id);
setTasksIds(tasksCopy);
});
} else {
setTasksIds([]);
}
}, [user]);
...
// keep the rest of the code and pass tasksIds instead of tasks to the flatlist
const Task = ({ taskId, memoizedOnPressPriority }) => {
const [task, setTask] = useState(null)
const { user } = useContext(AuthContext);
useEffect(() => {
if (user) {
// retrieve data by id so only the updated component will rerender
// I guess this will be something like this
return onValue(ref(database, `users/${user}/tasks/${taskId}`), (snapshot) => {
const todo = snapshot.val();
setTask(todo);
});
} else {
setTask(null);
}
}, [user]);
if (task === null) {
return null
}
// return the component like before

React native / Flatlist with Switches : they switch back immediately

I'm pretty new to React, trying to do something (I thought) simple, a list of parameters with switches.
But when I switch them, they immediately switch back to original display. I saw some posts around that, but couldn't solve the problem.
I also tried to put the Switch value in a state, but then I get the "hooks can only be called inside of the body of a function component" error.
Here is the code :
const UserPlantsPrefs = ({userPlantsPrefs}) => {
const [prefs, setPrefs] = useState([
{
"plant": "plant one",
"hasPref": true
},
{
"plant": "plant two",
"hasPref": true
}
]);
function toggleSwitch(value, index) {
setPrefs((prevPrefs) => {
prevPrefs[index].hasPref = value;
return prevPrefs;
})
}
function PrefItem({item, index}) {
return (
<View>
<Text>{item.plant}</Text>
<Switch
onValueChange={(value) => {
toggleSwitch(value, index);
}}
value={item.hasPref}
/>
</View>
)
}
return (
<View style={{backgroundColor: "white"}}>
<FlatList data={prefs} renderItem={PrefItem}/>
</View>
)
}
export default UserPlantsPrefs;
Your call to setPrefs inside toggleSwitch returns the same object is is given after modifying it in-place.
If the result of a setState call is == equal to its previous value, React will not re-render, so in order to cause a re-render, you need to create a new object.
Here would be one way of creating a new object with the value you want:
setPrefs((prevPrefs) => {
return {
...prevPrefs,
[index]: {
...prevPrefs[index],
hasPref: value
}
}
})
(This snippet uses spread syntax and computed-property initialisers, which you may want to look up - or you can use any other way of shallow-cloning the object to create a new one)

How to go back to previous scroll place when navigation to go back?

I got a productList page and product details page
In the product list page I got a scroll handler on the flatList which load more data when the end of the list is reached.
The product list component is something like this
let productList = useSelector(state => state.productReducer.productList);
const [numbersOfItem, setNumberOfItem] = useState(20);
const [itemList, setItemList] = useState(productList);
const handleLoadMoreData = async () => {
setLoadingMoreData(true);
try {
let requestNumber = numbersOfItem + 20;
setNumberOfItem(requestNumber);
await dispatch(
fetchProductList(
categoryTitle,
subCategoryName,
activeTab,
requestNumber,
),
);
} catch {
console.log('error');
} finally {
setLoadingMoreData(false);
}
};
<FlatList
data={itemList}
numColumns={2}
nestedScrollEnabled
columnWrapperStyle={styles.flatItemColumn}
scrollEnabled
scrollEventThrottle={16}
snapToAlignment="start"
decelerationRate={'fast'}
showsHorizontalScrollIndicator={false}
renderItem={renderIndividualItem}
keyExtractor={(item, index) => String(index)}
onEndReached={() => handleLoadMoreData()}
/>
After Scrolling for a while when I see a product I wanna explore I will goes into the product details page
Inside the product details page when I click navigation.goBack().
The scroll position in the productList is go back to the place of 20th item. I think its because the default display is setNumberOfItem is set to 20.
How can I change this behavior.
Adding an Api Call in a focus callBack in the screen that you want to go back to, can help solve this problem.(ProductList)
React.useEffect(() => {
fetchData();//func to get the data
const willFocusSubscription = props.navigation.addListener('focus', () => {
fetchData();//call it here as well
});
return willFocusSubscription;
}, []);
Use theinitialScrollIndex prop of the FlatList component to order the index where you want to start the FlatList. And find out how to know from which screen you come from when your productList page get focused

Passing function as a param in react-navigation 5

NOTE: This query is for react-navigation 5.
In react navigation 4 we could pass a function as a param while navigating but in react navigation 5, it throws a warning about serializing params.
Basically, what I am trying to do is, navigate to a child screen from parent screen, get a new value and update the state of the parent screen.
Following is the way I am currently implementing:
Parent Screen
_onSelectCountry = (data) => {
this.setState(data);
};
.
.
.
<TouchableOpacity
style={ styles.countrySelector }
activeOpacity={ 0.7 }
onPress={ () => Navigation.navigate("CountrySelect",
{
onSelect: this._onSelectCountry,
countryCode: this.state.country_code,
})
}
>
.
.
.
</TouchableOpacity>
Child Screen
_onPress = (country, country_code, calling_code) => {
const { navigation, route } = this.props;
navigation.goBack();
route.params.onSelect({
country_name: country,
country_code: country_code,
calling_code: calling_code
});
};
Passing a callback through react native navigation params is not recommended, this may cause the state to freeze (to not to update correctly). The better solution here would be using an EventEmitter, so the callback stays in the Screen1 and is called whenever the Screen2 emits an event.
Screen 1 code :
import {DeviceEventEmitter} from "react-native"
DeviceEventEmitter.addListener("event.testEvent", (eventData) =>
callbackYouWantedToPass(eventData)));
Screen 2 code:
import {DeviceEventEmitter} from "react-native"
DeviceEventEmitter.emit("event.testEvent", {eventData});
useEffect(() => {
return () => {
DeviceEventEmitter.removeAllListeners("event. testEvent")
};
}, []);
Instead of passing the onSelect function in params, you can use navigate to pass data back to the previous screen:
// `CountrySelect` screen
_onPress = (country, country_code, calling_code) => {
const { navigation, route } = this.props;
navigation.navigate('NameOfThePreviousScreen', {
selection: {
country_name: country,
country_code: country_code,
calling_code: calling_code
}
});
};
Then, you can handle this in your first screen (in componentDidUpdate or useEffect):
componentDidUpdate(prevProps) {
if (prevProps.route.params?.selection !== this.props.route.params?.selection) {
const result = this.props.route.params?.selection;
this._onSelectCountry(result);
}
}
There is a case when you have to pass a function as a param to a screen.
For example, you have a second (independent) NavigationContainer that is rendered inside a Modal, and you have to hide (dismiss) the Modal component when you press Done inside a certain screen.
The only solution I see for the moment is to put everything inside a Context.Provider then use Context.Consumer in the screen to call the instance method hide() of Modal.

How to properly update/re-render a component that is not a child React Native?

I'm using a react-navigation. More specifically, I have a materialTabNavigator nested inside of a drawerNavigator. Each tab is in itself a stackNavigator. I have a button in homeScreen, that navigates to makePost.js. There I take in information and store it to Async storage using a simple wrapper.
In Posts.js there's a FlatList displaying each post as a component. The data for the FlatList is initially set correctly after making a request from Async Storage. The problem is that this only happens when the app is first opened. I have tried many different approaches to solve this. The only way so far I've found is to continuously setState in ComponentDidUpdate() in Posts.js. Obviously this is problematic, because it re-renders constantly. I can set a flag to stop is from rendering, but then it will not re-render again.
Ultimately, what I'd like to happen is that when I hit the user is done entering their information and is ready to make a post, they hit the button in makePost.js, and the data in the FlatList of Posts.js is update.
I've tried to pass parameters using navigation, does not work, parameters get lost somewhere, probably because of the nested navigators.
I could really used some guidance on the proper way to accomplish this.
( Navigators; not sure why this is forcing to one line )
---drawer
--tabNav
-home
homeScreen.js
makePost.js
-posts
posts.js
-messages
--drawer1
--drawer2
//Posts.js
export default class Posts extends React.Component {
state = {
rows: [
{id: 0, text: "dog"},
],
}
componentDidMount() {
this.loadState();
}
loadState = () => {
var value = store.get('posts').then((res => {
if (res === null) {
res = [{id: 0, text: "default"}]
} else {
res = res
}
this.setState({rows: res})
}))
}
componentDidUpdate() {
this.loadState();
}
renderItem = ({item}) => {
return (
<BoardTab style={styles.row} />
)}
render() {
return (
<View style={styles.view}>
<FlatList
ListFooterComponent={this.renderFooter}
style={styles.container}
data={this.state.rows}
renderItem={this.renderItem}
keyExtractor={extractKey}
>
</FlatList>
<BoardScreenFooter />
</View>
);
}
And Posts.js button looks like this:
<TouchableOpacity
onPress={ () => {
this._onPressButton
this.storeFunc(this.state.newPost)
const retval = this.state.rows
this.props.navigation.navigate('Board',
{rowsID: retval});
}
}>
<Icon
reverse
name='md-camera'
type='ionicon'
color='green'
size={12}
/>
</TouchableOpacity>
storeFunc(newObj) {
newObj.id = newObj.id + 1
store.push('posts', newObj)
store.get('posts').then((res) => {
this.setState({rows: res})
})
}
Rapidly, i would say: use Redux. It alloq you to have global state in your app, which mean you can access the state anywhere (And also set them anywhere)
When opening the app, you get the data from the AsyncStore into the Redux store. You listen to the redux state (Which will be a props in your component) and display your list. When modifying your list in the other tab, you need to do 2 things:
Store the new data in the AsyncStorage
Update the state in the redux store. Since Posts.js will be listening at the redux store (as a props), it will re-render each time your data will change
A simple way to re-render a React-Navigation screen view on navigating to it:
All credit goes to Andrei Pfeiffer, Jul 2018, in his article: "Handle Tab changes in React Navigation v2" https://itnext.io/handle-tab-changes-in-react-navigation-v2-faeadc2f2ffe
I will reiterate it here in case the above link goes dead.
Simply add a NavigationEvents component to your render function with the desired listener prop:
render() {
return (
<View style={styles.view}>
<NavigationEvents
onWillFocus={payload => {
console.log("will focus", payload);
this.loadState();
}}
/>
<FlatList
ListFooterComponent={this.renderFooter}
style={styles.container}
data={this.state.rows}
renderItem={this.renderItem}
keyExtractor={extractKey}
>
</FlatList>
<PostScreenFooter />
</View>
);
}

Resources