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.
Related
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
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)
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!
I'm trying to render a react component which is populated by data from a fetch-request to my backend API, but when i add multiple components, all components end up with the data from the last request.
I use redux and react, along with flow. I'm successful in fetching the information from my backend-server and I have made sure that i receive the expected information. I am able to set the information in the component once the state changes, but all components end up with the same information. Here is my code:
My action:
export function fetchStates(order_id) {
return dispatch => api.post(`${order_id}/states`)
.then((response) => {
dispatch({ type: 'FETCH_STATES_SUCCESS', response });
});
}
code from my component-file:
type Props = {
states: Array<State>,
order_id: number,
fetchStates: () => void,
}
//... and then inside the component
componentDidMount() {
this.props.fetchStates(this.props.order_id)
}
// This function is called from the render() method
renderStates() {
return this.props.states.map((state) =>
<div style={{ flex: '1' }}>
{state.name}
</div>
);
}
// Connecting the component
export default connect(
state => ({
states: state.states.states,
}),
{ fetchStates }
)(States);
And my reducer:
const initialState = {
states: [],
};
export default function (state = initialState, action) {
switch (action.type) {
case 'FETCH_STATES_SUCCESS':
return {
...state,
states: action.response.data,
};
default:
return state;
}
}
And to create the components i use:
<States order_id={ order.id }/>
Expected results:
Component 1: state1, state2, state3 (from fetch 1)
Component 2: state4, state5, state6 (from fetch 2)
Component 3: state7, state8, state9 (from fetch 3)
Actual results:
Component 1: state7, state8, state9 (from fetch 3)
Component 2: state7, state8, state9 (from fetch 3)
Component 3: state7, state8, state9 (from fetch 3)
I am fairly new to react and redux as well but believe I see the problem in your renderStates method.
renderStates() { return this.props.states.map((state) =>
<div style={{ flex: '1' }}>
{state.name}
</div> ); }
You do not assign a unique key to each div created so react is unable to update each one individually causing all to be updated when the state changes, resulting in the last update (state7, state8, state9) to show in all components.
I believe the fix would be along these lines:
renderStates() { return this.props.states.map((state) =>
<div key={state.id} style={{ flex: '1' }}>
{state.name}
</div> ); }
The key (no pun intended) is providing a value that will be unique to each state to the key property. That may be an id field if it exists in your case, name.ToString() or some other value.
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>
);
}