React native / Flatlist with Switches : they switch back immediately - reactjs

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)

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 rerendering UI after using array.map

I am attempting to have an icon switch its visual when clicked (like a checkbox). Normally in react native I would do something like this:
const [checkbox, setCheckbox] = React.useState(false);
...
<TouchableHighlight underlayColor="transparent" onPress={() => {setCheckbox(!setCheckbox)}}>
{added ? <MaterialIcons name="playlist-add-check" size={40} />
: <MaterialIcons name="playlist-add" size={40} />}
</TouchableHighlight>
However I have made some changes, and now I can't seem to replicate this behavior. I am using AsyncStorage class to storage and get arrays of objects for display. For simplification, in the example below I removed the storage code, and the objects each have an 'id' and an 'added' attribute, which is essentially the boolean value of the checkbox.
I am now attempting to update the icon shown to the user whenever it is pressed. I know the function is being called, but it will not update the icon. I am using array.map to create the list of icons. I created a demo here, and the code is below: https://snack.expo.dev/#figbar/array-map-icon-update
const templateObject = {
id: 0,
added: false,
};
const templateObject2 = {
id: 1,
added: true,
};
export default function App() {
const [savedNumbers, setSavedNumbers] = React.useState([]);
React.useEffect(() => {
setSavedNumbers([templateObject,templateObject2]);
}, []);
const populateSavedNumbers = () =>
savedNumbers.map((num, index) => <View key={index}>{renderPanel(num.id,num.added)}</View>);
const updateNumber = (id) => {
let capturedIndex = -1;
for(var i = 0; i < savedNumbers.length; i += 1) {
if(savedNumbers[i].id === id) {
capturedIndex = i;
break;
}
}
let _tempArray = savedNumbers;
_tempArray[capturedIndex].added = !_tempArray[capturedIndex].added;
setSavedNumbers(_tempArray);
}
const renderPanel = (id:number, added:boolean) => {
return (
<View>
<TouchableHighlight underlayColor="transparent" onPress={() => {updateNumber(id);}}>
{added ? <MaterialIcons name="playlist-add-check" size={40} />
: <MaterialIcons name="playlist-add" size={40} />}
</TouchableHighlight>
</View>
);
}
return (
<View>
<View>buttons:</View>
<View>{populateSavedNumbers()}</View>
</View>
);
}
This is a common React pitfall where things don't re-render when it seems like they should. React does shallow comparisons between new and old states to decide whether or not to trigger a re-render. This means that, when declaring a variable to simply equal a state variable which is an object or an array, a re-render is not triggered since those two variables now reference the same underlying data structure.
In this case, you are setting _tempArray to reference the array savedNumbers rather than creating a new array. Therefore, React's shallow comparison comes back as "equal", and it doesn't believe that a re-render is necessary.
To fix this, change this line:
let _tempArray = savedNumbers;
to this:
let _tempArray = [...savedNumbers];

Redux state not updating even though reducer is called

I having a restaurant type application that I'm building. I'm using redux for handling the state. I have an icon in the top corner that keeps track of the number of items in the cart. This worked and was updated properly when the state contained an array. I have since change the state to a Map just for my own personal reasons and everything works EXCEPT the number is no longer being updated. I can see that the reducer is still doing the work however the number isn't updating like before. I've tried to look for error and still cannot find where it is going wrong.
My reducer:
import { MenuAction } from "../components/Utils";
const CartItems = (state : Map<Item, number> = new Map(), action: MenuAction) : Map<Item, number> => {
console.warn(state);
switch (action.type) {
case 'ADD_TO_CART':
if (state.has(action.payload)) {
return state.set(action.payload, state.get(action.payload) + 1);
} else {
return state.set(action.payload, 1);
}
case 'REMOVE_FROM_CART':
if (state.has(action.payload)) {
if (state.get(action.payload) == 1) {
state.delete(action.payload);
return state;
} else {
return state.set(action.payload, state.get(action.payload) - 1);
}
}
}
return state
}
export default CartItems
The component with the icon that displays the number:
const ShoppingCartIcon = (props: any) => (
<View style={[{ padding: 5 }, Platform.OS == 'android' ? styles.iconContainer : null]}>
<View>
<Text style={{color: 'white', fontWeight: 'bold'}}>
{Utils.getCartTotal(props.cartItems)}
</Text>
</View>
<Icon onPress={() => props.navigation.navigate('Cart')} name="ios-cart" size={30} />
</View>
)
const mapStateToProps = (state: Map<Item, number>) => {
return {
cartItems: state
}
}
export default connect(mapStateToProps)(withNavigation(ShoppingCartIcon));
The problem is that you're doing a state-mutation which is against Redux principles. Although the state values appear to be updated in your proceeding code, the changes were being made to the same, initial object in reference. That is the problem with using new Map() as your initial state, you end up using methods that mutate state like .set():
state.set(action.payload, state.get(action.payload) + 1)
Redux stresses the concept of immutability. https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns. As in do not make alterations to state, because it does not register as new data - so it finds no need to re-render your carts component with the updated numbers. To get your connected-component to re-render we need a completely new redux-state.
To achieve your desired outcome, you should revert it back to a simple array [] and use methods like .map() and .filter() that helps you create a brand-new copy of state.

React redux updates flatlist item but turns item into an Array

I am using react redux with react native, I have a flatlist of photos. When I click the like button, I'd like to update only a single photo in the flatlist. The following code seems to work, and the photo's like status is updated, but somehow my feed is messed up after the update. Before the update, this.props.feed[index] is a single object, after the update, this.props.feed[index] becomes an array of objects, what am I doing wrong? I got the idea from: https://stackoverflow.com/a/47651395/10906478
But it also seems really inefficient to loop through all items of flatlist to find one that matches the passed in photoId. Is there a better way?
Screen:
toggleLike = async(photoId, index) => {
console.log("before: ", this.props.feed[index]);
await this.props.toggleLike(photoId);
console.log("after: ", this.props.feed[index]);
}
...
<FlatList
data = {this.props.feed}
keyExtractor = {(_, index) => index.toString()}
renderItem = {({item, index}) => (
<View key={index}>
<TouchableOpacity onPress={()=> this.toggleLike(item.photoId, index)}>
<Text>Button</Text>
</TouchableOpacity>
</View>
)}
/>
Action
export const toggleLike = (photoId) => async(dispatch) => {
dispatch({type: "UPDATE_ITEM", photoId: photoId})
}
Reducer
export default (state = initialState, action) => {
switch(action.type) {
case "UPDATE_ITEM":
return {...state, feed: [state.feed.map((item,_) => {
if (item.photoId === action.photoId) {
return { ...item, liked: !item.liked };
}
return { ...item };
})]};
// other cases
You're calling map inside an array, which returns a nested array:
return {...state, feed: /* Here ->*/[state.feed.map((item,_) => {
if (item.photoId === action.photoId) {
return { ...item, liked: !item.liked };
}
return { ...item };
})]};
This should do:
return {
...state, // Current state
feed: state.feed.map((item, _) => { // map returns a new array
if (item.photoId === action.photoId) {
item.liked = !item.liked;
}
return item;
})
}
For the feed being an array, look closely at your code. You'll see you have wrapped the value of feed in square brackets and run a map on the array. So feed is an array, and the map is also an array. This is why you have an array of objects at each index point for state.feed. I'd normally suggest you get rid of the surrounding square brackets and let your map create the array.
However, that isn't actually the root cause of the issue, and there is a more thorough solution.
If the need is to find the matching ID and update the "liked" value without disturbing the order of the array, try using findIndex instead of map on the array. Find the index where your item is and update just that value. You might need to make a clone of the array and inner objects if it complains about directly mutating a Redux store value.
Best of luck!

Calling a non-returning JSX function inside of render() in React

I got a quick question on calling a function inside of the render method or some potential way to update a method when a user decides to go to the next screen via clicking "Send".
My goal is to change the old created "selectedExchange" to an updated "selectedExchange" as the user taps the "Send" arrow to go to the next screen.
// On Send Functionality
onSend = () => {
const { exchanges } = this.props
// Testing to see if hard coded values get sought out response
// Hard code values work
// this.props.selectExchange updates the selectedExchange
this.props.selectExchange(exchanges.exchanges[0])
this.props.navigation.navigate('selectRecipient', { transition: 'slideToLeft' })
//return ( exchange )
}
// Additional helper method, able to replace the above "this.props.selectExchange()"
// if necessary
updateExchange = (value, exchange) => {
this.props.selectExchange(exchange)
this.setState({ selectedDisplayName: value })
// Render call
render() {
const { navigation, account, exchanges } = this.props
const { selectedExchange } = exchanges
const { status, symbolPriceTicker, dayChangeTicker } = selectedExchange
const loading = status === 'pending' && !symbolPriceTicker && !dayChangeTicker
const avatar = account.profile_img_url ? { uri: account.profile_img_url } : IMAGES.AVATAR
return (
<View style={globalStyles.ROOT}>
<Loading loading={loading} />
<Image style={globalStyles.backgroundImg} source={IMAGES.BACKGROUND} />
<TopArea navigation={navigation} avatar={avatar} />
<ScrollView>
{
exchanges.exchanges.length > 0 &&
exchanges.exchanges.map(exchange => (
<View style={screenStyles.container}>
<ServiceInfo
account={account}
balances={exchange.balances}
displayName={exchange.label}
symbolPriceTicker={symbolPriceTicker}
// onRequest={this.onRequest}
onSend={this.onSend}
/>
...
...
What I've tried:
Passing an exchange prop via "onSend={this.onSend(exchange)}" to see if I could pass the necessary object that would be used to update selectedExchange. This didn't work as it required me to return something from onSend.
Directly calling the helper method in the JSX between various views. Also didn't work as it required I returned some form of JSX.
Not sure how else I could tackle this. Thanks for any help!

Resources