useSelector doesnot update the UI - reactjs

I have a nested state like :
bookingDetails = {
jobCards: [
{
details_id: '1',
parts: [
{...},
{...}
]
}
]}
Now I got the respective jobCards in component from props from parent component i.e detailsID by using useSelector:
const jobCard = useSelector(state => state.bookingDetails.jobCards.find(item => item.details_id === detailsID))
I got a button that successfully adds new object in parts in respective jobCards but that doesnot update the UI.
My bookingDetails Reducer:
case 'ADD_PARTS':
return {
...state,
jobCards: state.jobCards.map(jobCard => {
if (jobCard.details_id === action.id) {
jobCard.parts = [...jobCard.parts, { _id: uuid(), name: '' }]
}
return jobCard
})
}

use like this
const [isJobUpdated, setIsJobUpdated] = useState(false);
const jobCard = useSelector(state => state.bookingDetails.jobCards.find(item => item.details_id === detailsID))
useEffect(() => {
setIsJobUpdated(!!jobCard.length);
}, [jobCard])
return (
<>
{isJobUpdated && <YourComponent />
</>
)
NOTE: this is not the best way to do. You might face re-render issue. Just to check if this solve your current issue.

Forgot to add return statement.
The reducer should have been:
case 'ADD_PARTS':
return {
...state,
jobCards: state.jobCards.map(jobCard => {
if (jobCard.details_id === action.id) {
return {
...jobCard,
parts: [...jobCard.parts, { id: uuid(), name: ''}]
}
}
return jobCard
})
}

Related

react redux thunk not populating state object

im having an issue with my code, its not populating the state object when state action is being performed. im new with redux
i have this code. so far that having an issue
this is the statement that will called the props.action fetchProjectFamilyList
case 'SubBusinessUnit':
setProductFamilyDetailsObj([])
if (selectedOption.id != 0) {
props.actions.fetchDepartment(selectedOption.id)
props.actions.fetchProjectFamilyList(selectedOption.id)
console.log(props)
}
setDropdownDataInState(resetData, 'Department')
setFormFields({
...formFields,
'OtherNamedInsuredIndustry': {
...formFields.OtherNamedInsuredIndustry,
value: ''
},
'NamedInsuredIndustry': {
...formFields.NamedInsuredIndustry,
value: "",
selectedId: 0
},
[fieldName]: {
...formFields[fieldName],
value: selectedOption.description, selectedId: selectedOption.id
}
});
break;
and this is the code for the commonreducer
export const fetchProjectFamilyList = createAsyncThunk(types.FETCH_PROJECT_FAMILY_LIST,
async (option, {getState, rejectWithValue}) => {
const reduxThunkConfig = {
checkStateData:getState().commonReducer.projectFamilyList && getState().commonReducer.projectFamilyList[option],
rejectWithValue
}
const APIConfig = {
URL: "eapi-referencedata/v1/lists/12?filterBySourceList=" + option + "&filterBySourceListValue=15",
method:"getData",
}
console.log('fetchProjectFamilyList')
return fetchCachedData(reduxThunkConfig, APIConfig);
}
)
im using the builder in my case of course inistailstate is set
const initialState = {
projectFamilyList:{},
}
builder.addCase(fetchProjectFamilyList.fulfilled, (state, action) => {
const subDivision = action.meta.arg;
return {
...state,
projectFamilyList:{
...state.projectFamilyList,
[subDivision]: action.payload},
}})
const commonActions = { ...actions, fetchProjectFamilyList }
export { commonActions, commonReducer}
this is the comment that accept the state as props. but the props productFamilyDetailsObj is empty object
<ProductFamilyComponent
productFamilyDetailsObj={productFamilyDetailsObj}
/>
function ProductFamilyComponent({ productFamilyDetailsObj }) {
return <div className="boxLayout">
<p className="smallHeading">Product Families</p>
{productFamilyDetailsObj.map((text, textIndex) => {
let index = textIndex;
return ( .... and so on
I hope theres someone who could help me resolving this. thank in advance.

Is this the correct way to update state?

My Component looks like this:
import cloneDeep from "clone-deep";
import { Context } from "../../context";
const Component = () => {
const context = useContext(Context);
const [state, setState] = useState(
{
_id: "123",
users: [
{
_id: "1",
points: 5
},
{
_id: "2",
points: 8
}
]
}
);
useEffect(() => {
context.socket.emit("points");
context.socket.on("points", (socketData) => {
setState(prevState => {
const newState = {...prevState};
const index = newState.users
.findIndex(user => user._id == socketData.content._id);
newState.users[index].points = socketData.content.points;
return newState;
})
});
return () => context.socket.off("points");
}, []);
return <div>(There is table with identificators and points)</div>
};
I wonder if this is the right approach. I just want to write the code in the right way.
Or maybe it's better with the use of deep cloning? Does it matter?
setState(prevState => {
const newState = cloneDeep(prevState);
const index = newState.users
.findIndex(user => user._id == "2");
newState.users[index].points++;
return newState;
})
EDIT: I added the rest of the code to make it easier to understand.
In your current code:
useEffect(() => {
setState((prevState) => {
const newState = { ...prevState };
const index = newState.users.findIndex((user) => user._id == "2");
newState.users[index].points++;
console.log({ prevState, newState });
return newState;
});
}, []);
You can see that prevState is being mutated (points is 9):
{
_id: "123",
users: [
{
_id: "1",
points: 5
},
{
_id: "2",
points: 9 // Mutated!
}
]
}
To avoid mutating the state, you have to use not mutating methods such as spread operator or map function:
useEffect(() => {
setState((prevState) => {
const newState = ({
...prevState,
users: prevState.users.map((user) =>
user._id === "2"
? {
...user,
points: user.points + 1
}
: user
)
})
console.log({ prevState, newState });
return newState
}
);
}, []);
Now you can see that the prevState is not mutated:
{
_id: "123",
users: [
{
_id: "1",
points: 5
},
{
_id: "2",
points: 8 // Not mutated :)
}
]
}
Your code will work, but the problem will start when your state becomes bigger.
You currently have only two properties on the state, the _id and the users. If in the future will add more and more properties like loggedUser and settings and favorites, and more... your application will render everything on every state change.
At this point you will have to start thinking about other solutions to state management, like redux, mobx, or just split the state to smaller useState, also you can look into useReducer in complex structures.

++ increments value by 2 instead of 1

I am using the following reducer to add products, delete them, and add them to a cart. I would like the 'ADD_TO_CART' method when called to add an item to the cart, but if an item is already in the cart, I would like to increase it by 1. However, the method is increasing cart items by 2 instead of 1
export default (state, action) => {
switch (action.type) {
case 'ADD_PRODUCT':
return {
...state,
products: [action.payload, ...state.products]
}
case 'DELETE_PRODUCT':
return {
...state,
products: state.products.filter(product => product.id !== action.payload)
}
case 'ADD_TO_CART': if (state.cart.some(cartItem => cartItem.id === action.payload.id)) {
const updatedItemIndex = state.cart.findIndex(cartItem => cartItem.id === action.payload.id)
let cartItems = [...state.cart]
let itemToUpdate = cartItems[updatedItemIndex]
itemToUpdate.quantity++
return { ...state, cart: cartItems }
} else {
const newItem = { ...action.payload, quantity: 1 }
return { ...state, cart: [...state.cart, newItem] }
}
default:
return state
}
}
This is the code dispatching the action
function addToCart(product) {
dispatch({
type: 'ADD_TO_CART',
payload: product
})
}
I am calling the action from the following component
import React, { useContext } from 'react'
import { ProductContext } from './ProductContext'
export const ProductCard = ({ product }) => {
const { deleteProduct, addToCart } = useContext(ProductContext)
const handleDelete = () => {
deleteProduct(product.id)
}
const handleCart = () => {
addToCart(product)
}
return (
<div className='product__card'>
<h3>{product.productName}</h3>
<p>{product.price}</p>
<button className='button'>View Product</button>
<button onClick={handleCart} className='button'>Add to Cart</button>
<button onClick={handleDelete} className='button'>Delete</button>
</div>
)
}
Try changing the quantity from a number to an empty quote.
Old: const newItem = { ...action.payload, quantity: 1 }
New: const newItem = { ...action.payload, quantity: '' }
You have a mistake. You MUTATE your state.
let cartItems = [...state.cart] // <-- reinitialize only the array,
// the array items remains with the same reference!!!!
let itemToUpdate = cartItems[updatedItemIndex]
itemToUpdate.quantity++ // <--- this will mutates your state!!!
return { ...state, cart: cartItems }
Fix: create a new item, and copy the data with the spread operator, and delete the old one.
I have ran into the same problem with a very similar situation, and none of the current answers helped me. Therefore I can only recommend
itemToUpdate.quantity++
Be changed into
itemToUpdate.quantity += 0.5;
I know that this is not the correct way to solve this problem, but the functionality works, and you can change it later when you will find the solution

I want to process setState at once

enter image description here
I want to count "indie" and "action" at the same time when the button is clicked. However, the only real application is "action". Please tell me how.
This is my solution to your problem
import React, { useState, useEffect } from "react";
const games = [
{ id: 1, genre: ["indie", "action"] },
{ id: 2, genre: ["indie"] },
{ id: 3, genre: ["action"] }
];
function ButtonComponent(props) {
const { genre, fn } = props;
return <button onClick={() => fn(genre)}>Click</button>;
}
function TestPage() {
const [genre, setGenre] = useState({ indie: 0, action: 0 });
const addGenrecount = (genres) => {
setGenre((previousState) => {
let { indie, action } = previousState;
genres.forEach((genre) => {
if (genre === "indie") indie = indie + 1;
if (genre === "action") action = action + 1;
});
return { indie, action };
});
};
useEffect(() => console.log("genre", genre), [genre]); // Logs to the console when genre change
return games.map((game) => {
const { id, genre } = game;
return <ButtonComponent key={id} genre={genre} fn={addGenrecount} />;
});
}
export default TestPage;
You may also go to codesandbox to test the demo
https://codesandbox.io/s/xenodochial-dirac-q01h4?file=/src/App.js:0-968
Just Friendly Tip:
If you need help regarding react I recommend to upload your code to codesandbox so that we can easily reproduce or solve the problem

How to safely update my state when I have to traverse and lookup/remove items in my state

I need to modify my state and I am unsure how to do it correctly.
My account property in my state looks something like this:
{
"account":{
"id":7,
"categories":[
{
"id":7,
"products":[
{
"productId":54
}
]
},
{
"id":9,
"products":[
{
"productId":89
}
]
}
]
}
}
My action dispatches the following:
dispatch({
type: Constants.MOVE_PRODUCT,
productId: 54,
sourceCategoryId: 7,
targetCategoryId: 9
});
Now my reducer skeleton is:
const initialState = {
account: null,
};
const accounts = (state = initialState, action) => {
switch (action.type) {
case Constants.MOVE_PRODUCT:
/*
action.productId
action.sourceCategoryId
action.targetCategoryId
*/
const sourceCategoryIndex = state.account.categories.findIndex((category) => { return category.id === action.sourceCategoryId; });
const sourceCategory = state.account.categories[sourceCategoryIndex];
const targetCategoryIndex = state.account.categories.findIndex((category) => { return category.id === action.targetCategoryId; });
const targetCategory = state.account.categories[targetCategoryIndex];
// ??
return {...state};
}
}
export default accounts;
I am confused, if I update the state directly inside of the switch block, is that wrong?
Does it have to be a one-liner update that does the mutation in-place or as long as I do it in the switch block it is fine?
Update
From the action, I need to remove the productId from the sourceCategoryId and add it to the targetCategoryId inside of the account state object.
Yes, you should not be doing state.foo = 'bar' in your reducer. From the redux docs:
We don't mutate the state. We create a copy with Object.assign(). Object.assign(state, { visibilityFilter: action.filter }) is also wrong: it will mutate the first argument. You must supply an empty object as the first parameter. You can also enable the object spread operator proposal to write { ...state, ...newState } instead.
So your reducer could look like
function accountsReducer (state = initialState, { sourceCategoryId, productId }) {
const targetProduct = state.categories
.find(({ id }) => id === sourceCategoryId)
.products
.find(({ id }) => id === productId);
switch (action.type) {
case Constants.MOVE_PRODUCT:
return {
...state,
categories: state.categories.reduce((acc, cat) => {
return cat.id !== sourceCategoryId
? {
...acc,
cat: { ...cat, products: cat.products.filter(({ id }) => id !== productId) }
}
: {
...acc,
cat: { ...cat, products: [...cat.products, targetProduct] }
}
}, {});
};
}
}
But this a pain...you should try to normalize your data into a flat array.
// first, let's clean up the action a bit
// type and "payload". I like the data wrapped up in a bundle with a nice
// bow on it. ;) If you don't like this, just adjust the code below.
dispatch({
type: Constants.MOVE_PRODUCT,
payload: {
product: { productId: 54 }
sourceCategoryId: 7,
targetCategoryId: 9
}
});
// destructure to get our id and categories from state
const { id, categories } = state
// map the old categories to a new array
const adjustedCategories = categories.map(cat => {
// destructure from our payload
const { product, sourceCategoryId, targetCategoryId } = action.payload
// if the category is the "moving from" category, filter out the product
if (cat.id === sourceCategoryId) {
return { id: cat.id, products: [...cat.products.filter(p => p.productId !== product.productId)
}
// if the category is our "moving to" category, use the spread operator and add the product to the new array
if (cat.id === targetCategoryId) {
return { id: cat.id, products: [...cat.products, product] }
}
)
// construct our new state
return { id, categories: adjustedCategories }
This solution keeps the function pure and should give you what you want. It's not tested, so may not be perfect.
You could take the following approach:
const accounts = (state = initialState, action) => {
switch (action.type) {
case Constants.MOVE_PRODUCT:
// Extract action parameters
const { productId, sourceCategoryId, targetCategoryId } = action
// Manually "deep clone" account state
const account = {
id : state.account.id,
categories : state.account.categories.map(category => ({
id : category.id,
products : category.products.map(product => ({ productId : product.productId })
}))
}
// Extract source and target categories
const sourceCategory = account.categories.find(category => category.id === sourceCategoryId);
const targetCategory = account.categories.find(category => category.id === targetCategoryId);
if(sourceCategory && targetCategory) {
// Find product index
const index = sourceCategory.products.findIndex(product => (product.productId === action.productId))
if(index !== -1) {
const product = sourceCategory.products[index]
// Remove product from source category
sourceCategory.products.splice(index, 1)
// Add product to target category
targetCategory.products.splice(index, 0, product)
}
}
return { account };
}
}
Here is the ugly solution :)
const accounts = (state = initialState, action) => {
switch (action.type) {
case Constants.MOVE_PRODUCT:
const sourceCategoryIndex = state.account.categories.findIndex(
el => el.id === action.sourceCategoryId
);
const targetCategoryIndex = state.account.categories.findIndex(
el => el.id === action.targetCategoryId
);
const sourceCategory = state.account.categories.find(
el => el.id === action.sourceCategoryId
);
const targetCategory = state.account.categories.find(
el => el.id === action.targetCategoryId
);
const itemToMove = sourceCategory.products.find(
el => el.productId === action.productId
);
const newSourceCategory = {
...sourceCategory,
products: sourceCategory.products.filter(
el => el.productId !== action.productId
)
};
const newTargetCategory = {
...targetCategory,
products: [...targetCategory.products, itemToMove]
};
const newCategories = Object.assign([], state.account.categories, {
[sourceCategoryIndex]: newSourceCategory,
[targetCategoryIndex]: newTargetCategory
});
return { ...state, account: { ...state.account, categories: newCategories } };
}
};
Phew :) As a learner it's quite good for me :) But, I like #Daniel Lizik's approach, using reduce.
Here is the working example:
const action = {
productId: 54,
sourceCategoryId: 7,
targetCategoryId: 9,
}
const state = {
"account":{
"id":7,
"categories":[
{
"id":7,
"products":[
{
"productId":54,
},
{
"productId":67,
},
]
},
{
"id":9,
"products":[
{
"productId":89,
}
]
}
]
}
};
const sourceCategoryIndex = state.account.categories.findIndex( el => el.id === action.sourceCategoryId );
const targetCategoryIndex = state.account.categories.findIndex( el => el.id === action.targetCategoryId );
const sourceCategory = state.account.categories.find( el => el.id === action.sourceCategoryId );
const targetCategory = state.account.categories.find( el => el.id === action.targetCategoryId );
const itemToMove = sourceCategory.products.find( el => el.productId === action.productId );
const newSourceCategory = {...sourceCategory, products: sourceCategory.products.filter( el => el.productId !== action.productId ) };
const newTargetCategory = { ...targetCategory, products: [ ...targetCategory.products, itemToMove ] };
const newCategories = Object.assign([], state.account.categories, { [sourceCategoryIndex]: newSourceCategory,
[targetCategoryIndex]: newTargetCategory }
);
const newState = { ...state, account: { ...state.account, categories: newCategories } };
console.log( newState );

Resources