React native component rerender when using context api - reactjs

I'm currently learning React Native. I have an aplication that is used to track expenses and I'm setting some dummy data as initial state. The app works fine, I can add new expenses, edit and delete them. Also, I have a component that is showing recent expenses for the last 7 days only.
Today I noticed that my recent list was empty, because all of the expenses were older than the last 7 days, so I changed the dates of a few items so I would have some data on initial load.
After saving the changes, I noticed that the app refreshed, but the state didn't, I still had an empty list. Only when I forced the reload of the app, the state was updated.
So my question is, is this intended? Should the component rerender after I manually change the initial state or not? I couldn't find any similar issues and also I couldn't find anything in the docs.
Here's my context code:
import { createContext, useReducer } from 'react';
const DUMMY_EXPENSES = [
{
id: 'e1',
description: 'A pair of shoes',
amount: 59.99,
date: new Date('2021-12-19'),
},
{
id: 'e2',
description: 'A pair of trousers',
amount: 89.29,
date: new Date('2022-01-02'),
},
{
id: 'e3',
description: 'Bananas',
amount: 19.99,
date: new Date('2023-02-06'),
},
{
id: 'e4',
description: 'Book',
amount: 69.69,
date: new Date('2023-02-05'),
},
];
export const ExpensesContext = createContext({
expenses: [],
addExpense: ({ description, amount, date }) => {},
deleteExpense: (id) => {},
updateExpense: (id, { description, amount, date }) => {},
});
function expensesReducer(state, action) {
switch (action.type) {
case 'ADD':
const id = new Date().toString() + Math.random().toString();
return [{ ...action.payload, id: id }, ...state];
case 'UPDATE':
const expenseIndex = state.findIndex((e) => e.id === action.payload.id);
const expenses = [...state];
expenses[expenseIndex] = { id: action.payload.id, ...action.payload.data };
return expenses;
case 'DELETE':
const expenseArray = [...state];
return expenseArray.filter((e) => e.id !== action.payload);
default:
return state;
}
}
export default function ExpensesContextProvider({ children }) {
const [expenses, dispatch] = useReducer(expensesReducer, DUMMY_EXPENSES);
function addExpense(expenseData) {
dispatch({ type: 'ADD', payload: expenseData });
}
function deleteExpense(id) {
dispatch({ type: 'DELETE', payload: id });
}
function updateExpense(id, expenseData) {
dispatch({ type: 'UPDATE', payload: { id: id, data: expenseData } });
}
const value = {
expenses: expenses,
addExpense: addExpense,
updateExpense: updateExpense,
deleteExpense: deleteExpense,
};
return <ExpensesContext.Provider value={value}>{children}</ExpensesContext.Provider>;
}

Related

How to store and get Cart Items to localstorage with Redux?

I'm using Redux toolkit and adding product items to the cart using redux in my eCommerce app, products are adding to the cart perfectly also I tried to store the added products to the localstorage which is also working fine but the thing is How can I use those items from localstorage to display the <CartItems> even after refreshing the page.
Here is my reducer:-
const productItemsSlice = createSlice({
name: "product",
initialState: {
items: [],
totalQuantity: 0,
localStorageItems: [],
},
reducers: {
addProduct(state, action) {
const newItem = action.payload;
const existingItem = state.items.find((item) => item.id === newItem.id);
state.totalQuantity++;
if (!existingItem) {
state.items.push({
id: newItem.id,
price: newItem.price,
quantity: 1,
totalPrice: newItem.price,
name: newItem.name,
image: newItem.image,
category: newItem.category,
});
} else {
existingItem.quantity = existingItem.quantity + 1;
existingItem.totalPrice = existingItem.totalPrice + newItem.price;
}
// LOCAL STORAGE
localStorage.setItem("list", JSON.stringify(state.items));
state.localStorageItems = JSON.parse(localStorage.getItem("list"));
},
},
});
And here I'm trying to access those setted cartItems to the localstorage:-
//<CartItems />
//Here state.productItem is from configureStore reducer object:-
// const store = configureStore({
// reducer: {
// productItem: productItemsSlice.reducer,
// }
// })
const productItems = useSelector((state) => state.productItem.localStorageItems);
const items = productItems.map((el) => {
return (
<CartItems
key={el.id}
item={{
id: el.id,
name: el.name,
image: el.image,
price: el.price,
category: el.category,
totalPrice: el.totalPrice,
quantity: el.quantity,
}}
/>
);
});
Please suggest me how can I achieve this, also if I remove items from cart.
Remove localStorageItems from initialState and add totalQuantity key for number of quantity to calculate how many items are added. Now we have to set localStorage for all these 3 initialState with different token key.
Outside of slice function we parse Items:-
const items =
localStorage.getItem("cartList") !== null
? JSON.parse(localStorage.getItem("cartList"))
: [];
const totalAmount =
localStorage.getItem("cartTotal") !== null
? JSON.parse(localStorage.getItem("cartTotal"))
: 0;
const totalQuantity =
localStorage.getItem("cartQuantity") !== null
? JSON.parse(localStorage.getItem("cartQuantity"))
: 0;
// adding this function to prevent repear code
const setCartListFunc = (items, totalAmount, totalQuantity) => {
localStorage.setItem("cartList", JSON.stringify(items));
localStorage.setItem("cartTotal", JSON.stringify(totalAmount));
localStorage.setItem("cartQuantity", JSON.stringify(totalQuantity));
};
This would be the initialState:-
initialState: {
items: items,
totalQuantity: totalQuantity,
totalAmount: totalAmount,
},
Now addProduct reducer should be like this to store the data to localStorage:-
addProduct(state, action) {
const newItem = action.payload;
const existingItem = state.items.find((item) => item.id === newItem.id);
state.totalQuantity++;
if (!existingItem) {
state.items.push({
id: newItem.id,
price: newItem.price,
quantity: 1,
totalPrice: newItem.price,
name: newItem.name,
image: newItem.image,
category: newItem.category,
});
} else {
existingItem.quantity = existingItem.quantity + 1;
existingItem.totalPrice = existingItem.totalPrice + newItem.price;
}
// added totalAmount to calculate number of items
state.totalAmount = state.items.reduce(
(total, items) => total + Number(items.price) * Number(items.quantity),
0
);
// Using function for all initialState
setCartListFunc(
state.items.map((item) => item),
state.totalAmount,
state.totalQuantity
);
},

What happens when a reducer returns 'state' in React?

If I have a contactReducer that looks like this:
import {
GET_CONTACTS,
DELETE_CONTACT,
ADD_CONTACT,
EDIT_CONTACT,
GET_CONTACT,
} from "../actions/types";
// the state the holds the contacts
const initialState = {
contacts: [
{
id: 1,
name: "John Doe",
email: "john#gmail.com",
phone: "555-555-5555",
},
{
id: 2,
name: "Karen Williams",
email: "karen#gmail.com",
phone: "444-444-4444",
},
{
id: 3,
name: "Henry Johnson",
email: "henry#gmail.com",
phone: "333-333-333",
},
],
contact: {},
testProp: {},
};
export default function (state = initialState, action) {
switch (action.type) {
case GET_CONTACTS:
console.log("get contacts");
return {
...state,
};
case DELETE_CONTACT:
return {
...state,
contacts: state.contacts.filter(
(contact) => contact.id !== action.payload
),
};
case ADD_CONTACT:
let newArray = state.contacts.slice(); // get the current contacts array
newArray.unshift(action.payload); //push on the new contact to the beg of array
return {
...state, //take the existing state..
contacts: newArray,
};
case EDIT_CONTACT:
console.log("trying to edit");
return {
...state,
contacts: state.contacts.map((contact) =>
contact.id == action.id ? (contact = action.payload) : contact
),
};
case GET_CONTACT:
const selectedContact = getSingleContactFromId(state, action.payload);
console.log("look here");
console.log(selectedContact);
return {
...state,
contact: selectedContact,
testProp: { test: "test prop" },
};
default:
console.log("testing action in default");
return state;
}
}
function getSingleContactFromId(state, id) {
var contact;
console.log("get single contact");
for (var i = 0; i < state.contacts.length; i++) {
contact = state.contacts[i];
if (contact.id == id) {
return contact;
}
}
}
What is actually happening when the reducer returns? Where does it return to? For example, I send a dispatch to the reducer like this this.props.addContact(newContact);
But, I don't see that I do anything with the returned object anywhere after this. In another file, is where I grab things from the state, so does return really just mean it is updating the state?
Assuming you use combineReducers, the returned state from a specific reducer will now be the updated state of the state chunk represented by this reducer.
Then, any connected component will receive the new state and will re-render.
This is a high-level description obviously.
More information can be found here: https://react-redux.js.org/using-react-redux/connect-mapstate

React Hooks & redux cause unnecesary rendering and some unexpected effects

Expected behavior - I load an photo object and render it in my component.
With the code below I do achieve the result, but with some unexpected effects which in the end make this component not useful - see further description in two parts.
I really want to dive in the cause of all of it and to understand how can I prevent such behavior in my app.
I did try some suggestions from others peoples similar problems, but nothing did helped. I won't list here the things I've tried, because, obviously, everytime I tried - I did something wrong since it didn't help me.
I will be grateful for any ideas and suggestions - I miss something and can't understand what is it.
Part 1 of the problem.
While loading this component for the first time and/or refreshing it - I get multiple rerenders. From the Redux DevTools I can observ that the actions fire for two times, console-logging any received from the photo value shows that this value appears in the console 6 times (first 3 times - with initial state from the redux-store, after - with the expected fetched from the photo object value).
Part 2 of the problem.
When I open the next photo (the same component, just passing different match.params.id) - the component starting to rerender apparently for random times. It might take some seconds to complete this rerender loop, so it rerenders sometimes for dozens, sometimes for more then a 100 time, but always in the end is rendering the needed info.
Analyzing the logs I saw that the the values of fetched now photo are just switching in the loop with the values of the photo fetched before. The looping stops with the correct values. And where from the previos values are coming - I can't figure out, because before fetching a new photo object I clear all the data of the previous in the redux state.
Component:
//IMPORTS
const Photo = ({ getPhotoById, photo, loading, match }) => {
const [photoData, setPhotoData] = useState({
photoID: match.params.id,
imgUrl: '',
photoFileName: '',
title: '',
description: '',
albumID: '',
albumName: '',
categoryID: '',
categoryName: '',
categoryID2: '',
categoryName2: '',
categoryID3: '',
categoryName3: '',
locationID: '',
locationName: '',
contributorID: '',
contributorName: '',
contributorWeb: '',
source: '',
sourceWeb: '',
author: '',
periodID: '',
periodName: '',
license: ''
});
const {
photoID,
imgUrl,
photoFileName,
title,
description,
albumID,
albumName,
categoryID,
categoryName,
categoryID2,
categoryName2,
categoryID3,
categoryName3,
locationID,
locationName,
contributorID,
contributorName,
source,
sourceWeb,
author,
periodID,
periodName,
license
} = photoData;
useEffect(() => {
getPhotoById(photoID);
}, [getPhotoById, photoID]);
useEffect(() => {
if (loading === false) {
const {
photoID,
imgUrl,
photoFileName,
title,
description,
albumID,
albumName,
categoryID,
categoryName,
categoryID2,
categoryName2,
categoryID3,
categoryName3,
locationID,
locationName,
contributorID,
contributorName,
source,
sourceWeb,
author,
periodID,
periodName,
license
} = photo;
setPhotoData({
photoID,
imgUrl,
photoFileName,
title,
description,
albumID,
albumName,
categoryID,
categoryName,
categoryID2,
categoryName2,
categoryID3,
categoryName3,
locationID,
locationName,
contributorID,
contributorName,
source,
sourceWeb,
author,
periodID,
periodName,
license
});
}
}, [loading]);
useEffect(() => {
if (!loading) {
initOpenseadragon();
}
}, [loading]);
console.log(photoFileName, 'photoFileName');
const initOpenseadragon = () => {
OpenSeadragon({
id: 'viewer',
tileSources: `/uploads/tiles/${photoFileName}.dzi`,
prefixUrl: '/images/osd/',
showZoomControl: true,
showHomeControl: true,
showFullPageControl: true,
showRotationControl: true
});
};
return !photo && !loading ? (
<NotFound />
) : (
<Fragment>
SOME JSX
</Fragment>
);
};
Photo.propTypes = {
getPhotoById: PropTypes.func.isRequired,
// photo: PropTypes.object.isRequired,
loading: PropTypes.bool.isRequired
};
const mapStateToProps = state => {
return {
photo: state.photo.photo,
loading: state.photo.loading
};
};
export default connect(
mapStateToProps,
{ getPhotoById }
)(Photo);
ACTION:
export const getPhotoById = photo_id => async dispatch => {
try {
dispatch({ type: CLEAR_PHOTO });
dispatch({ type: LOAD_PHOTO });
const res = await axios.get(`/api/photo/${photo_id}`);
dispatch({
type: GET_PHOTO,
payload: res.data
});
} catch (err) {
dispatch({
type: PHOTOS_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
REDUCER
const initialState = {
photo: null,
photos: [],
loading: true,
error: {}
};
const photo = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_PHOTO:
return {
...state,
photo: payload,
loading: false
};
case LOAD_PHOTO:
return {
...state,
loading: true
};
case CLEAR_PHOTO:
return {
...state,
photo: null,
loading: false
};
case PHOTOS_ERROR:
return {
...state,
error: payload,
loading: false
};
default:
return state;
}
};
export default photo;
Your problem is that you are adding getPhotoById as a dependency of your hook see this article about the dependency array.
If you want to prevent the re-render you can do the following:
const ref = useRef();
getPhotoByIdRef.current = getPhotoById
useEffect(() => {
getPhotoByIdRef(match.params.id)
}, [getPhotoByIdRef, match.params.id]);

Redux state is being updated without dispatching any action

I should start off by saying this is not a duplicate of this question, which happened to have the same title.
I'm simply getting a customers object of arrays from props inside a componentDidMount method like this;
componentDidMount() {
const { customers } = this.props
const expiringCustomers = getExpiringCustomers(customers)
console.log('Expiring customers ', expiringCustomers)
}
Inside another file, I have that getExpiringCustomers function which takes the customers passed and is suppose to return a newly modified list of customers like this;
function numbersOnly(value) {
if(_.isString(value)) {
value = Number(value.replace(/[^\d]/g, ''))
}
return value
}
function normalizeNumber(collection, field) {
return collection.map(obj => {
obj[field] = numbersOnly(obj[field])
return obj
})
}
export function getExpiringCustomers(customers) {
const expiringCustomers = customers.filter(customer => {
const daysLeft = Number(new Date(customer.endDate)) - _.now()
if(daysLeft <= (dateInMonth * 3)) {
return customer
}
})
return normalizeNumber(expiringCustomers, 'rent')
}
I'm connecting my react component with redux state like this;
const mapStateToProps = state => ({
customers: state.customers.filter(customer => customer && !customer.deleted)
})
export default connect(mapStateToProps)(Accounting)
Problem
After the functions run and log results, customers' state is changed in redux store.
This is very confusing as customers_edit action has to pass through some procedures but none of them are called/logged.
Snapshot of the affected object:
Ps. The data is just boilerplate.
//- Focus on rent property
const customers = [
...,
{
id: 'o91wukyfsq36qidkld02a0voo93rna5w',
cardId: 'GD-1101010111',
id_type: 'Driving License',
firstName: 'Maalim',
lastName: 'Guruguja',
names: 'Maalim Guruguja',
property: '5iaprurefg3v3uhad688mypo9kqf6xk3',
rent: '250,000',
email: 'tonimarikapi#yahoo.com',
phone: '239-288-3838-38',
noticePeriod: '3',
status: '2 months remain',
startDate: '2018-07-09',
endDate: '2018-08-17',
createdAt: 1530623480772,
updatedAt: 1531213159147
},
...
]
//- After the functions run, log and edit customers array
const customers = [
...,
{
id: 'o91wukyfsq36qidkld02a0voo93rna5w',
cardId: 'GD-1101010111',
id_type: 'Driving License',
firstName: 'Maalim',
lastName: 'Guruguja',
names: 'Maalim Guruguja',
property: '5iaprurefg3v3uhad688mypo9kqf6xk3',
rent: 250000,
email: 'tonimarikapi#yahoo.com',
phone: '239-288-3838-38',
noticePeriod: '3',
status: '2 months remain',
startDate: '2018-07-09',
endDate: '2018-08-17',
createdAt: 1530623480772,
updatedAt: 1531213159147
},
...
]
From the linked question (possible duplicate one) the guy who answered stated that it's some mutation issue that may cause this. I'm not sure if that applies on props that are suppose to be read-only.
How can I stop these functions from updating my redux store, please help.
You mutate the objects in normalizeNumber, since all the array methods you use don't clone the array's objects.
Change normalizeNumber callback to return a new object with the updated field:
function normalizeNumber(collection, field) {
return collection.map(obj => ({
...obj,
[field]: numbersOnly(obj[field])
}))
}
It looks like you're modifying the customers array unintentionally.
Try:
componentDidMount() {
const { customers } = { ...this.props };
const expiringCustomers = getExpiringCustomers(customers)
console.log('Expiring customers ', expiringCustomers)
}

Filter products depend on another ACTION in React-native Redux

I have an app which get all categories and products from the server with Redux ACTIONS. I need to filter products with a category Id. after load data action is complete, i call another action to filter products but i'm a little bit confused.
There is codes of few parts of the app:
ProductsActions:
export const GET_INITIAL_PRODUCTS_DATA = "GET_INITIAL_PRODUCTS_DATA";
export const GET_INITIAL_PRODUCTS_DATA_RESULT = "GET_INITIAL_PRODUCTS_DATA_RESULT";
export const GET_INITIAL_PRODUCTS_DATA_ERROR = "GET_INITIAL_PRODUCTS_DATA_ERROR";
export const FILTER_PRODUCTS_BY_CATEGORY_ID = "FILTER_PRODUCTS_BY_CATEGORY_ID";
export const getInitialProductsData = () => ({
type: GET_INITIAL_PRODUCTS_DATA
});
export const filterProductsByCategoryId = categoryId => ({
type: FILTER_PRODUCTS_BY_CATEGORY_ID,
categoryId
});
ProductsReducers:
import {
GET_INITIAL_PRODUCTS_DATA,
GET_INITIAL_PRODUCTS_DATA_RESULT,
GET_INITIAL_PRODUCTS_DATA_ERROR,
FILTER_PRODUCTS_BY_CATEGORY_ID
} from "../actions/products";
const initialState = {
isFetching: false,
data: {},
error: null
};
const filterProductsByCategoryId = (state, action) => {
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case GET_INITIAL_PRODUCTS_DATA:
return {
...state,
isFetching: true
};
case GET_INITIAL_PRODUCTS_DATA_RESULT:
return {
...state,
isFetching: false,
data: action.result
};
case GET_INITIAL_PRODUCTS_DATA_ERROR:
return {
...state,
isFetching: false,
error: action.error
};
case FILTER_PRODUCTS_BY_CATEGORY_ID:
return {
...state,
data: filterProductsByCategoryId(state, action.categoryId)
};
default:
return state;
}
};
export default reducer;
And there is my code to call filter action:
filterProducts = (title = "A") => {
const _categories = Object.values(this.props.categories);
const selectedCategory = _categories.find(
category => category.title === title
);
this.props.dispatch(filterProductsByCategoryId(selectedCategory.id));
My questions is:
A) Is there is a way to filter my data and display them in UI and refresh them without using ACTIONS way??
B) If A's answer is No!, How can i get my state.data and filter them in FILTER_PRODUCTS_BY_CATEGORY_ID?
Thanks.
You can use the Array.prototype.filter() to return filtered result.
keep in mind that this will return an array and not a single value, which is a good thing if you are using this filter within your reducer. because your reducer's shape is an array and not an object.
Running example:
const myData = [{
name: 'some name',
id: 1
}, {
name: 'some name2',
id: 2
}, {
name: 'some name3',
id: 3
}, {
name: 'some name4',
id: 4
}]
const filterProductsByCategoryId = (state, action) => {
return state.filter(c => c.id === action.categoryId);
};
const result = filterProductsByCategoryId(myData, {categoryId: 2});
console.log(result);
I think it is more appropriate to create a selector for a singular product that will handle this kind of action, this way you will be able to return an object instead of an array with one product in it.
Not to mention the benefits of using reselect to do some memoizations.
For this task you can use the Array.prototype.find():
const myData = [{
name: 'some name',
id: 1
}, {
name: 'some name2',
id: 2
}, {
name: 'some name3',
id: 3
}, {
name: 'some name4',
id: 4
}]
const filterProductsByCategoryId = (state, id) => {
return state.find(c => c.id === id);
};
const result = filterProductsByCategoryId(myData, 2);
console.log(result);

Resources