Spread syntax for Map in JavaScript - javascript

In my React project (Chat App), I am using redux toolkit, I want to use Map in place of object literal {} as state.
chatMap state contains contactId (mongodb _id) as key and chat array as value.
something like that -
chatMap =
{
[contactId1]: [chatsArr1],
[contactId2]: [chatsArr2]
}
(chatSlice.js)
const initialState = {
chatMap: new Map(),
...
}
reducers: {
setChatMap: (state, action) => {
state.chatMap = {...chatMap, key1: action.payload}
},
...
}
How to update chatMap (Map) state ?

This is what you're looking for:
reducers: {
setChatMap: (state, action) => {
state.chatMap = new Map([...chatMap, [key1, action.payload]])
},
...
}

You can do something like this.
reducers: {
setChatMap: (state, action) => {
state.chatMap = new Map(state.chatMap).set('key1', action.payload)
},
...
}

Firt you can convert the map to an object, then spread it. Again convert the final object to a map.
reducers: {
setChatMap: (state, action) => {
const obj = object.fromEntries(chatMap);
const tCM = {...obj, key1: action.payload};
state.chatMap = new Map(tCM);
},
...
}

Your problem relies on the fact that, React has now way of knowing if your object(Map) has changed or not, since its a bit more complicated than a primitive, you need to make what's called a deep-copy of that object.
By passing a new object, react will see a new reference and then trigger a new render.
Cheers 😉

Related

Remove value from an array nested in an object also nested in an array

I have a problem with my code. I currently have some data like the one below;
users: [
{
name: 'bolu',
features: ['Tall'],
},
{
name: 'cam',
features: ['Bearded', 'Short'],
},
],
};
What I am trying to do is delete/remove a single feature - for example if I pass in 'short' into my redux action. I'd like for it (the 'Short' text) to be removed from the features array. I currently have my redux action set up this way:
export interface UsersDataState {
name: string,
features: Array<string>,
}
export interface UsersState {
users: UsersDataState[];
}
const initialState: UsersState = {
users: [],
};
export const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
removeUser: (state, action: PayloadAction<string>) => {
const removedUsers = state.users.filter((user) => user.features.indexOf(action.payload));
state.users = removedUsers;
},
},
});
So here I am passing in the value in (action.payload is the value being passed in). When this action is dispatched, I want to remove just the word that is passed in from the features array. I hope this is clearer now.
This doesn't work for some reason and I am unable to figure out why. Any help would be appreciated please, thank you.
Your code doesn't match your state structure. Replace traits with users, and values with features.
It looks like that's a part of a reducer, not an action (which is an object, not a function).
You should be returning a new state from the reducer.
Given your update the function should be called removeFeature.
So, I've corrected a few bits of your code based on what I remember from Redux. Note: contrived example.
// State
const state={users:[{name:"joe",features:["Mean","Short"]},{name:"bolu",features:["Tall"]},{name:"cam",features:["Bearded","Short"]}]};
// The reducer accepts a state, and an action
function reducer(state, action) {
// We destructure the type, and payload, from the action object
const { type, payload } = action;
// Depending on the type...
switch (type) {
case 'removeFeature': {
// `map` over the users (we return a new state array)
return state.users.map(user => {
// `filter` out the feature elements
// that match the payload
const updatedFeatures = user.features.filter(feature => {
return feature !== payload;
});
// Return a new updated object
return { ...user, features: updatedFeatures };
});
}
default: return state;
}
}
const updatedState = reducer(state, {
type: 'removeFeature',
payload: 'Short'
});
console.log(updatedState);

Redux toolkit filter() not working when wanting to display new array at the same time

I am currently building a clothing shop in which you can add products to the cart, and delete each one of them as you like and it makes the cart re-render and display a new cart without that product within it.
so I've made a Slice for cart in redux. the 'addProduct' part works fine, but the 'deleteProduct' reducer which uses filter() doesn't work. (when i click my delete button nothing happens, and no changes in difference in Redux Devtools)
my slice:
const selectedProductsSlice = createSlice({
name:'selectedProducts',
initialState:{
productList:[],
checkoutPrice:0,
},
reducers: {
addProduct:(state,{payload}) => {
state.productList.push(payload)
state.checkoutPrice += payload.price
},
deleteProduct:(state,{payload}) => {
state.productList.filter((foundProduct) => foundProduct.id !== payload.id)
}
}});
my button and handleDelete:
<button className="btn-delete" onClick={handleDelete(product)}>Delete</button>
function handleDelete(p) {
console.log(p)
dispatch(deleteProduct(p.product))
}
edit:
filter didnt work in every possible way i tried. so i changed my way and did this instead to work. but still i wonder why didnt filter() method work properly.
deleteProduct: (state, { payload }) => {
const foundIndex = state.productList.findIndex((p) => {
return p.product.id === payload.id
})
state.productList.splice(foundIndex,1)
}
The filter operation creates a new array without changing the existing instance. Therefore you need to assign it to state.
deleteProduct: (state, { payload }) => {
return {
...state,
productList: state.productList.filter(
foundProduct => foundProduct.id !== payload.id
)
};
};
You also need to change the way you call the handleDelete in the onClick handler.
onClick={() => handleDelete(product)}
Filter methods returns new array so you have to update your existing array tool. Please update your slice:
const selectedProductsSlice = createSlice({
name:'selectedProducts',
initialState:{
productList:[],
checkoutPrice:0,
},
reducers: {
addProduct:(state,{payload}) => {
state.productList.push(payload)
state.checkoutPrice += payload.price
},
deleteProduct:(state,{payload}) => {
state.productList=state.productList.filter((foundProduct) => foundProduct.id !== payload.id)
}
}});
I went through exactly same problem as you.
I solved this problem just to move my reducer to another existing reducer
Hope you to get through it. I couldn't pass it by

adding objects to collections elements using spread syntax

I'm working with react redux and want use spread syntax on a reducer in order to return and updated object which contains a collection element where I want to add objects.
I have the following reducer logic which is not working because the collection is being filled with the payload object values instead of the object itself:
export default function(state = INITIAL_STATE, action){
switch (action.type){
case CREATE:
return {...INITIAL_STATE,collection:{...state.collection,...action.payload}};
default:
return state;
}
}
The payload is an object like:
{
'DG67QBXQJW7':{
key:'12345',
price:'15',
name:'test'
}
};
And the returned state I would like to have something like:
{
name:'items',
collection:{
'DG67QBXQJW7':{
key:'12345',
price:'15',
name:'test'
},
'FB843VUV3N9':{
key:'67890',
price:'11',
name:'test2'
}
}
}
But I got with my previous showed logic something like:
{
name:'items',
key:'12345',
price:'15',
name:'test'
}
If someone could help or show me an advanced and good spread syntax guide for this kind of operations would be great.
Thanks in advance.
You just need to use your current state, not INITIAL_STATE, as the base of your object.
return {...state, collection: {...state.collection, ...action.payload}};
Test example:
const state = { name: 1, collection: { test3: { a: 3}, test4: {a: 4}}};
const payload = { test5: { a: 5 }};
console.log({...state, collection: {...state.collection, ...payload}});

How do I only update one value of my reducer in Redux?

Below is my code
function customMsg(state, action) {
state = state || {
person: {
isFetching: false,
didInvalidate: false,
name: "",
height: "",
}
};
switch(action.type) {
case ACTION_TYPES.PERSON.FETCH_PERSOn_CONTENT_SUCCESS:
return $.extend({}, state, {
person.name: action.result.name
});
default:
return state;
}
}
How do I only update one value of my reducer in Redux?
Above is a example which i only want to update the name of person object.
How can i do that?
Using jQuery extend, create a clone and set person name:
var newState = $.extend({}, state);
newState.person.name = action.result.name;
return newState;
Otherwise, to clone deeply an object you can use lodash cloneDeep().
Another way is to use immutableJS to set your app state as immutable. It is much more bug "mutable" proof and offers functions to set deep nested value in an immutable. See updateIn:
return state.updateIn(['person', 'name'], () => action.result.name);
Try it!

How to update single value inside specific array item in redux

I have an issue where re-rendering of state causes ui issues and was suggested to only update specific value inside my reducer to reduce amount of re-rendering on a page.
this is example of my state
{
name: "some name",
subtitle: "some subtitle",
contents: [
{title: "some title", text: "some text"},
{title: "some other title", text: "some other text"}
]
}
and I am currently updating it like this
case 'SOME_ACTION':
return { ...state, contents: action.payload }
where action.payload is a whole array containing new values. But now I actually just need to update text of second item in contents array, and something like this doesn't work
case 'SOME_ACTION':
return { ...state, contents[1].text: action.payload }
where action.payload is now a text I need for update.
You can use map. Here is an example implementation:
case 'SOME_ACTION':
return {
...state,
contents: state.contents.map(
(content, i) => i === 1 ? {...content, text: action.payload}
: content
)
}
You could use the React Immutability helpers
import update from 'react-addons-update';
// ...
case 'SOME_ACTION':
return update(state, {
contents: {
1: {
text: {$set: action.payload}
}
}
});
Although I would imagine you'd probably be doing something more like this?
case 'SOME_ACTION':
return update(state, {
contents: {
[action.id]: {
text: {$set: action.payload}
}
}
});
Very late to the party but here is a generic solution that works with every index value.
You create and spread a new array from the old array up to the index you want to change.
Add the data you want.
Create and spread a new array from the index you wanted to change to the end of the array
let index=1;// probably action.payload.id
case 'SOME_ACTION':
return {
...state,
contents: [
...state.contents.slice(0,index),
{title: "some other title", text: "some other text"},
...state.contents.slice(index+1)
]
}
Update:
I have made a small module to simplify the code, so you just need to call a function:
case 'SOME_ACTION':
return {
...state,
contents: insertIntoArray(state.contents,index, {title: "some title", text: "some text"})
}
For more examples, take a look at the repository
function signature:
insertIntoArray(originalArray,insertionIndex,newData)
Edit:
There is also Immer.js library which works with all kinds of values, and they can also be deeply nested.
You don't have to do everything in one line:
case 'SOME_ACTION': {
const newState = { ...state };
newState.contents =
[
newState.contents[0],
{title: newState.contents[1].title, text: action.payload}
];
return newState
};
I believe when you need this kinds of operations on your Redux state the spread operator is your friend and this principal applies for all children.
Let's pretend this is your state:
const state = {
houses: {
gryffindor: {
points: 15
},
ravenclaw: {
points: 18
},
hufflepuff: {
points: 7
},
slytherin: {
points: 5
}
}
}
And you want to add 3 points to Ravenclaw
const key = "ravenclaw";
return {
...state, // copy state
houses: {
...state.houses, // copy houses
[key]: { // update one specific house (using Computed Property syntax)
...state.houses[key], // copy that specific house's properties
points: state.houses[key].points + 3 // update its `points` property
}
}
}
By using the spread operator you can update only the new state leaving everything else intact.
Example taken from this amazing article, you can find almost every possible option with great examples.
This is remarkably easy in redux-toolkit, it uses Immer to help you write immutable code that looks like mutable which is more concise and easier to read.
// it looks like the state is mutated, but under the hood Immer keeps track of
// every changes and create a new state for you
state.x = newValue;
So instead of having to use spread operator in normal redux reducer
return {
...state,
contents: state.contents.map(
(content, i) => i === 1 ? {...content, text: action.payload}
: content
)
}
You can simply reassign the local value and let Immer handle the rest for you:
state.contents[1].text = action.payload;
Live Demo
In my case I did something like this, based on Luis's answer:
// ...State object...
userInfo = {
name: '...',
...
}
// ...Reducer's code...
case CHANGED_INFO:
return {
...state,
userInfo: {
...state.userInfo,
// I'm sending the arguments like this: changeInfo({ id: e.target.id, value: e.target.value }) and use them as below in reducer!
[action.data.id]: action.data.value,
},
};
Immer.js (an amazing react/rn/redux friendly package) solves this very efficiently. A redux store is made up of immutable data - immer allows you to update the stored data cleanly coding as though the data were not immutable.
Here is the example from their documentation for redux:
(Notice the produce() wrapped around the method. That's really the only change in your reducer setup.)
import produce from "immer"
// Reducer with initial state
const INITIAL_STATE = [
/* bunch of todos */
]
const todosReducer = produce((draft, action) => {
switch (action.type) {
case "toggle":
const todo = draft.find(todo => todo.id === action.id)
todo.done = !todo.done
break
case "add":
draft.push({
id: action.id,
title: "A new todo",
done: false
})
break
default:
break
}
})
(Someone else mentioned immer as a side effect of redux-toolkit, but you should use immer directly in your reducer.)
Immer installation:
https://immerjs.github.io/immer/installation
This is how I did it for one of my projects:
const markdownSaveActionCreator = (newMarkdownLocation, newMarkdownToSave) => ({
type: MARKDOWN_SAVE,
saveLocation: newMarkdownLocation,
savedMarkdownInLocation: newMarkdownToSave
});
const markdownSaveReducer = (state = MARKDOWN_SAVED_ARRAY_DEFAULT, action) => {
let objTemp = {
saveLocation: action.saveLocation,
savedMarkdownInLocation: action.savedMarkdownInLocation
};
switch(action.type) {
case MARKDOWN_SAVE:
return(
state.map(i => {
if (i.saveLocation === objTemp.saveLocation) {
return Object.assign({}, i, objTemp);
}
return i;
})
);
default:
return state;
}
};
I'm afraid that using map() method of an array may be expensive since entire array is to be iterated. Instead, I combine a new array that consists of three parts:
head - items before the modified item
the modified item
tail - items after the modified item
Here the example I've used in my code (NgRx, yet the machanism is the same for other Redux implementations):
// toggle done property: true to false, or false to true
function (state, action) {
const todos = state.todos;
const todoIdx = todos.findIndex(t => t.id === action.id);
const todoObj = todos[todoIdx];
const newTodoObj = { ...todoObj, done: !todoObj.done };
const head = todos.slice(0, todoIdx - 1);
const tail = todos.slice(todoIdx + 1);
const newTodos = [...head, newTodoObj, ...tail];
}
Pay attention to the data structure:
in a project I have data like this
state:{comments:{items:[{...},{...},{...},...]} and to update one item in items I do this
case actionTypes.UPDATE_COMMENT:
const indexComment = state.comments.items.findIndex(
(comment) => comment.id === action.payload.data.id,
);
return {
...state,
comments: {
...state.comments,
items: state.comments.items.map((el, index) =>
index === indexComment ? { ...el, ...action.payload.data } : el,
),
},
};
Note: in newer versions (#reduxjs/toolkit), Redux automatically detects changes in object, and you don't need to return a complete state :
/* reducer */
const slice = createSlice({
name: 'yourweirdobject',
initialState: { ... },
reducers: {
updateText(state, action) {
// updating one property will cause Redux to update views
// only depending on that property.
state.contents[action.payload.id].text = action.payload.text
},
...
}
})
/* store */
export const store = configureStore({
reducer: {
yourweirdobject: slice.reducer
}
})
This is how you should do now.

Categories

Resources