I am updating my redux state, and the state doesn't seem to be getting mutated, however the DOM is still not refreshing.
//update filters for events
setFilters = (name) => async () => {
const {onSetActiveEventTypes, authUser} = this.props;
let array = this.props.activeEventTypes
let index = array.indexOf(name);
if (index > -1) {
array.splice(index, 1);
}else {
array.push(name)
}
await Promise.resolve(onSetActiveEventTypes(array));
}
render() {
return <Accordion title="Filters" collapsed>
{
(this.props.eventTypes && this.props.activeEventTypes ?
<EventFilter eventTypes={this.props.eventTypes} activeEventTypes={this.props.activeEventTypes} action={this.setFilters}/>
: '')
}
</Accordion>
}
const mapStateToProps = (state) => ({
eventTypes: state.eventsState.eventTypes,
activeEventTypes: state.eventsState.activeEventTypes
});
const mapDispatchToProps = (dispatch) => ({
onSetEventTypes: (eventTypes) => dispatch({ type: 'EVENT_TYPES_SET',
eventTypes }),
onSetActiveEventTypes: (activeEventTypes) => dispatch({ type:
'ACTIVE_EVENT_TYPES_SET', activeEventTypes })
});
const authCondition = (authUser) => !!authUser;
export default compose(
withAuthorization(authCondition),
connect(mapStateToProps, mapDispatchToProps)
)(DashboardPage);
I have placed my code in my component above, it should be all that is needed to debug. I will put the reducer below
const applySetEventTypes = (state, action) => ({
...state,
eventTypes: action.eventTypes
});
const applySetActiveEventTypes = (state, action) => ({
...state,
activeEventTypes: action.activeEventTypes
});
function eventsReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case 'EVENT_TYPES_SET' : {
return applySetEventTypes(state, action);
}
case 'ACTIVE_EVENT_TYPES_SET' : {
return applySetActiveEventTypes(state, action);
}
default : return state;
}
}
export default eventsReducer;
Above is my reducer, I think I am following the correct patterns for managing redux state and maintaining immutability. What am I missing?
setFilters is a method that the checkboxes use to update active filters compared to all the filters available.
You are definitely mutating state:
const {onSetActiveEventTypes, authUser} = this.props;
let array = this.props.activeEventTypes
let index = array.indexOf(name);
if (index > -1) {
array.splice(index, 1);
}else {
array.push(name)
}
That mutates the existing array you got from the state, and then you are dispatching an action that puts the same array back into the state. So, you are both A) reusing the same array all the time, and B) mutating that array every time.
The approaches described in the Immutable Update Patterns page in the Redux docs apply wherever you are creating new state values, whether you're generating the new state in a reducer based on a couple small values, or before you dispatch the action.
//update filters for events
setFilters = (name) => async () => {
const {onSetActiveEventTypes, authUser} = this.props;
let array = []
this.props.activeEventTypes.map((type) =>{
array.push(type)
})
let index = array.indexOf(name);
if (index > -1) {
array.splice(index, 1);
}else {
array.push(name)
}
//use this once server sending active filters
// await eventTable.oncePostActiveEventTypes(authUser.email, array).then( data
=> {
// Promise.resolve(onSetActiveEventTypes(data));
// })
await Promise.resolve(onSetActiveEventTypes(array));
}
Related
Whenever I click add to cart button, the action is fired but redux state is not been updated (the initial state is not changing but the action is triggered).
const CartScreen = () => {
const { id } = useParams();
const { search } = useLocation();
const [searchParms] = useSearchParams();
const productId = id;
const qty = search ? Number(search.split("=")[1]) : 1;
const dispatch = useDispatch()
useEffect(() => {
if (productId){
dispatch(addToCart(productId, qty))
}
}, [dispatch, productId, qty])
return (
<div>
<h1>Add to CART</h1>
</div>
);
};
export default CartScreen
Cart action
export const addToCart = (id, qty) => async (dispatch, getState) =>{
const {data} = await axios.get(`http://127.0.0.1:8000/api/products/${id}`)
dispatch({
type: CART_ADD_ITEM,
payload:{
product:data._id,
name:data.name,
image:data.image,
countInStock:data.countInStock,
qty
}
})
localStorage.setItem('cartItems', JSON.stringify(getState().cart.cartItems))
}
Cart Reducer
export const cartReducer = (state = { cartItems: []}, action) =>{
switch(action.type){
case CART_ADD_ITEM:
const item = action.payload
const existItem = state.cartItems.findIndex(x => x.product === item.product)
if (existItem){
return{
...state,
cartItems: state.cartItems.map(x =>
x.product === existItem.product ? item : x)
}
} else{
return{
...state,
cartItems:[...state.cartItems, item]
}
}
default:
return state
}
}
Redux store
const reducer = combineReducers({
productList: productListReducer,
productDetails: productDetailsReducer,
cart: cartReducer,
})
const initialState = {
cart:{cartItems:cartItemsFromStorage}
};
const middleware = [thunk];
const store = createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
From redux dev tools I can see that the action I triggered. The item is getting to cart reducer because when I console.log item in const item=action.payload from the cartReducer, I get the particular item in Browser console, yet the cartItem redux state remains at the initial value, it's not updated
Array.prototype.find()- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
Array.prototype.findIndex()- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex
Using Array.prototype.findIndex() will basically look for and return the index of the first found item, and -1 if not found. While Array.prototype.find() returns the first element in the array that matched the criteria provided.
export const cartReducer = (state = { cartItems: [] }, action) => {
switch(action.type){
case CART_ADD_ITEM:
const item = action.payload;
// use Array.prototype.find() instead
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
const existItem = state.cartItems.find(x => x.product === item.product);
if (existItem){
return{
...state,
cartItems: state.cartItems.map(x =>
x.product === existItem.product ? item : x)
};
} else{
return{
...state,
cartItems: [...state.cartItems, item]
};
}
default:
return state;
}
};
The issue is that you are searching for an existing product and returning the found index (array.findIndex) then using the index value as a boolean condition (if (existItem) {...}).
This won't work as you are expecting since all non-zero numbers are truthy, while 0 is falsey. This means if no cart item products match that -1 is returned and the logic will treat this as an existing item. This is compounded later when updating the cart via array.map... if existItem is -1 this means there is no matching product, the new state.cartItems will be a new array, but it will not contain the new item object. In other words it will be just a copy of the previous state.
cartItems starts initially as an empty array, so existItem will always return -1 when first adding an item to the cart.
An additional unintentional bug occurs when a product does exist in the cartItems array and it's the 0th element. existItem will equal 0 and is thus falsey and item will be added to cartItems array as a duplicate.
#Chigbogu is correct regarding the use of array.findIndex and array.find, though I'd recommend using array.some if you are just checking the the cart items array has the item or not. This indicates you are working with a boolean explicitly. Rename existItem to hasItem or similar so the name also indicates a boolean value (this is by convention).
export const cartReducer = (state = { cartItems: []}, action) =>{
switch(action.type) {
case CART_ADD_ITEM: {
const newItem = action.payload;
const hasItem = state.cartItems.some(item => item.product === newItem.product);
if (hasItem) {
return {
...state,
cartItems: state.cartItems.map(item =>
item.product === newItem.product ? newItem : item
)
}
} else {
return {
...state,
cartItems: [...state.cartItems, newItem]
}
}
}
default:
return state;
}
};
Hi developers I am currently studying react js with redux for frontend and I want to implement state management (Redux) to my sample project. My Backend I use laravel. Now I already set the Action, Services, Reducers. When I try to console log the props state to my Component it shows that my action data response is null.
Problem: The action data response is null only.
Here is my MapState & mapDisPatch
const mapStateToProps = (state) => {
return {
filterChartRes: state.dashboard.filterChartRes,
}
}
const mapDisPatchToProps = (dispatch) => {
return {
loadFilterChartData: (selectionRange) => dispatch(loadFilterChartData(selectionRange)),
}
}
My Action:
export const loadFilterChartData = (selectionRange) => {
return (dispatch) => {
getFilterChartData(selectionRange).then((res) => {
console.log(res)
dispatch({ type: 'FILTER_CHART_RESPONSE', res })
},
error => {
dispatch({ type: 'FILTER_CHART_ERROR', error });
}
)
}
}
My Services:
export const getFilterChartData = (selectionRange) => {
const http = new HttpService();
//let filterData = selectionRange !== "" ? selectionRange : null;
let url = "auth/filter_chart";
return http.getData(url)
.then(data => {
return data;
})
}
My Reducers:
const initState = {
filterChartRes: null,
filterChartErr: null
};
const DashboardReducer = (state = initState, action) => {
switch (action.type) {
case 'FILTER_CHART_RESPONSE':
return {
...state,
filterChartRes: action.res.data
}
case 'FILTER_CHART_ERROR':
return {
...state,
filterChartErr: 'action.error'
}
default:
return state
}
}
export default DashboardReducer;
My Render:
const {filterChartRes } = this.props
console.log(filterChartRes, "My Filter");
My Work Output:
Back End Controller:
public function filter_chart() {
return 'Sample Data';
}
Hope Someone help on my problem
To solved this issue:
You must call the props inside the DidMount
componentDidMount = () => {
this.props.loadFilterChartData()
}
My component with filters doesn't re-render after changes in Redux state. With console.log() I can see that action and reducer works. ObjectFilter.js after changes gives good result with console, but doesn't re-render.
mapReducer.js
const mapReducer = (state = initState, action) => {
switch(action.type) {
case actions.SET_FILTERS:
console.log('SET_FILTERS', state)
return({
...state,
filters: action.filters
})
default:
return state;
}
}
export default mapReducer;
mapActions.js
export const setFilters = (el, old_filters) => {
let filters = old_filters;
let new_el = !old_filters[el];
filters[el] = new_el;
console.log(filters)
return (dispatch, getState) => {
dispatch({
type:actions.SET_FILTERS,
filters: filters
})
}
}
objectFilters.js
class ObjectFilters extends Component {
changeFilterHandler = (el) => {
this.props.setFilters(el, this.props.filters);
}
render () {
console.log(this.props.filters)
return (
/* some code */
);}
}
const mapDispatchToProps = dispatch => {
return {
setFilters: (el, filters) => dispatch(setFilters(el, filters))
}
}
const mapStateToProps = state => {
return {
filters: state.mapRedux.filters
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ObjectFilters);
The problem in your code is that you are mutating the old_filters directly, instead make a clone of it and then update filter value. Never mutate state and prop directly
export const setFilters = (el, old_filters) => {
let filters = {...old_filters}; // using spread operator to create a clone
let new_el = !old_filters[el];
filters[el] = new_el;
return (dispatch, getState) => {
dispatch({
type:actions.SET_FILTERS,
filters: filters
})
}
}
How can I use the mapdispatchtoprops function correctly to dispatch to reducer? First, I get data from the server and want to send this data to the reducer. firebaseChatData function cannot be transferred to the mapdispatchtoprops because it is inside the component
Messages.js
const MessageUiBody = ( { messages, loading } ) => {
const userData = JSON.parse(localStorage.getItem("user-data"));
useEffect( () => {
const firebaseChatData = () => (dispatch) => {
firebaseDB.ref().child(API.firebaseEnv + "/messages/messageItem" + userData.account_id)
.on("value", snap => {
const firebaseChat = snap.val();
// console.log(firebaseChat)
dispatch(firebaseChatAction(firebaseChat))
});
};
}, []);
return(
<div> // code </div>
);
};
//Action
const firebaseChatAction = (firebaseChat) => ({
type: 'FIREBASE_MESSAGE',
firebaseChat
});
const mapDispatchToProps = (dispatch) => {
return {
data : () => {
dispatch(firebaseChatData())
}
}
};
export default connect(null, mapDispatchToProps)(MessageUiBody)
Reducer
export default function messages ( state = [], action = {}) {
switch (action.type) {
case 'FIREBASE_MESSAGE' :
state.data.messages.push(action.firebaseChat);
return {
...state
};
default:
return state
}
}
You'll have to change your code, because you're defining data as the prop function that will dispatch your action:
const mapDispatchToProps = dispatch => {
return {
data: (result) => dispatch(firebaseChatAction(result)),
}
}
After that change the line after the console log in your promise and use the data prop that you defined in your mapDispatch function:
const MessageUiBody = ( { data, messages, loading } ) => {
const userData = JSON.parse(localStorage.getItem("user-data"));
useEffect( () => {
const firebaseChatData = () => (dispatch) => {
firebaseDB.ref().child(API.firebaseEnv + "/messages/messageItem" + userData.account_id)
.on("value", snap => {
const firebaseChat = snap.val();
// here you call the data that will dispatch the firebaseChatAction
data(firebaseChat)
});
};
}, []);
return(
<div> // code </div>
);
};
Also is worth to notice that you don't have to push items in your state, you can't mutate the current state, so always try to generate new items instead of modifying the existing one, something like this:
export default function messages ( state = [], action = {}) {
switch (action.type) {
case 'FIREBASE_MESSAGE' :
return {
...state,
data: {
...state.data,
messages: [...state.data.messages, action.firebaseChat]
}
};
default:
return state
}
}
With the spread operator you are returning a new array that contains the original state.data.messages array and will add the firebaseChat item as well.
I have the following connected component:
const selectFullNameById = (state, id) =>
{
const user = state.domain.people.values.find(u => u.id === id)
return `${user.lastName} ${user.firstName}`
}
const mapStateToProps = (state, ownProps) => {
const files = ownProps.files.map(f => ({
...f,
fullName: selectFullNameById(state, f.creatorId)
}))
return {
...ownProps,
files
}
}
export default connect(mapStateToProps)(Uploader)
The connected component receives an array with object literals as a Prop called files. Each object literal has a creatorId value, I want to pass to my presentational component the full name of this person, so I use a selector "selectFullNameById", to get these values. This works when this person is actually available in the global state.
But it could be that this person object literal is not yet present in the global state, and so a fetch should be done to the server to check what his name is. So I have an async action creator to do the lookup. But I don't know how to dispatch this from the selector.
I would do the following:
//1. create a default state for a person:
const DEFAULT_PERSON = {
id: -1,
fullName: 'n/a',
…
} //some useful stuff
//2. modify electFullNameById to
const selectFullNameById = (state, id) => {
const user = state.domain.people.values.find(u => u.id === id) || null;
//return default data, if user does not exist
return (user === null) ?
Object.assign({}, DEFAULT_PERSON).fullName :
`${user.lastName} ${user.firstName}`
}
//3. refactor mapStateToProps to:
const mapStateToProps = (state, ownProps) => {
const files = ownProps.files.map(f => ({
...f,
fullName: selectFullNameById(state, f.creatorId)
}))
//no need to add the ownProps here
return { files }
}
So the basic Idea is: Create some useful default data, which is used in case the user is not found. This way, you wont get any »undefined« error on the following usage of the data.
To dispatch an async action if a user if undefined can be added like so (assuming you are using redux thunk and fetch):
const loadUser = uid => dispatch => fetch(`${url}/${uid}`)
.then(response => response.json())
.then(json => dispatch({
type: 'user/loaded',
payload: { user: json }
}));
const YourComponent = ({ users, loadUser}) => {
return (
<div>
{
user.map(user => {
if (!user loaded) loadUser(user.id);
return (<some>html based on default data</html>)
})
}
</div>
);
}
You can dispatch the action right at rendering time since this action is async and so the re-rendering of the component wont be called right away, what would lead to recursion. If there are many users which have to be loaded it might be a good idea to filter them out and load them at once.