Change the values of all nested objects Immutable.js - javascript

reducer looks like this
import {ADD_TASK, EDIT_TASK, CHECK_TASK, CHECK_ALL_TASK} from '../constants';
import {OrderedMap, Record} from 'immutable';
const TaskRecord = Record({
id: null,
text: null,
isChecked: false
});
const ReducerState = Record({
entities: new OrderedMap({})
});
const defaultState = new ReducerState();
export default (tasksState = defaultState, action) => {
const {type, payload, randomId} = action;
switch (type) {
case ADD_TASK:
return tasksState.setIn(['entities', randomId], new TaskRecord({...payload.task, id: randomId}));
case CHECK_ALL_TASK:
return tasksState.map(entities => {
return entities.map((task) => {
task.set('isChecked', true);
});
});
};
return tasksState;
};
How change all isChecked in TaskRecord that in entities? I wrote CHECK_ALL_TASK, but it gives an error (tasksState undefined)

When you want to set a new value for a property based on it's current one, .update is what you want to use.
When you want to make a change to each item in a collection, .map is what you want to use.
In your case, you want to set a new value for the property entities by modifying each entity in the collection.
This means you want to use update and map:
function checkAllBoxes(taskState) {
return taskState.update('entities', entities =>
entities.map(entity =>
entity.set('isChecked', true)));
}
const TaskRecord = Immutable.Record({
id: null,
text: null,
isChecked: false
});
const ReducerState = Immutable.Record({
entities: new Immutable.OrderedMap({})
});
const taskState = new ReducerState({
entities: Immutable.OrderedMap({
a: new TaskRecord(),
b: new TaskRecord(),
c: new TaskRecord()
})
});
function checkAllBoxes(taskState) {
return taskState.update('entities', entities =>
entities.map(entity =>
entity.set('isChecked', true)));
}
console.log(checkAllBoxes(taskState))
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/4.0.0-rc.9/immutable.js"></script>

wrote this function, but can it be made as something simpler?
function checkAllCheckboxes(state) {
let newState = state;
for (let id of state.entities) {
newState = newState.setIn(['entities', id[0], 'isChecked'], true);
}
return newState;
}

Related

Dynamic getters in pinia - Vue 3

There is code for Vue 2 with Vuex 3.4:
getLink: (state, getters) => rel => {
let link = state.data?.getLinkWithRel(rel);
let stac = getters[rel];
let title = STAC.getDisplayTitle([stac, link]);
if (link) {
link = Object.assign({}, link);
link.title = title;
}
else if (stac instanceof STAC) {
return {
href: stac.getAbsoluteUrl(),
title,
rel
};
}
return link;
},
This code should be ported to Vue 3 with pinia. Did the following:
getLink: (state) => (rel) => {
let link = state.data?.getLinkWithRel(rel);
let stac = state[rel];
let title = STAC.getDisplayTitle([stac, link]);
if (link) {
link = { ...link };
link.title = title;
}
else if (stac instanceof STAC) {
return {
href: stac.getAbsoluteUrl(),
title,
rel
};
}
return link;
},
The stac variable contains either null or undefined values.
When using version 1, stac contains the data returned by the called getters[rel] function.
How do I write or force pinia to give data?
Additional information:
rel - contains a string with one of the getter names (parent or collection).
getter-parent:
parent: (state) => state.getStacFromLink('parent'),
Where getStacFromLink is also a getter.
getter-collection:
collection: (state) => state.getStacFromLink('collection'),
It looks like the parent or collection getters that are called do not get the state of the Store.

Need a custom assignment implementaion

I am working with some state management application where I have a data structure as follows
const mainObject = {
firstLevel: {
secondLevel: {
thirdLevel: {
actualProperty: 'Secret'
}
}
},
firstLevelUntouched:{
secondLevelUntouched:{
thirdLevelUntouched:{
untouchedProperty:'I don`t want to change'
}
}
}
};
I want to change the actualProperty to a new value which out a deepClone
I did it with the following code
const modified = {
...mainObject,
...{
firstLevel: {
...mainObject.firstLevel,
...{
secondLevel: {
...mainObject.firstLevel.secondLevel,
thirdLevel: {
...mainObject.firstLevel.secondLevel.thirdLevel,
actualProperty: 'New secret'
}
}
}
}
}
}
But its looks like Bulky Code. So I need to write a function like
modified = myCustomAssignment(mainObject, ['firstLevel', 'secondLevel', 'thirdLevel', 'actualProperty'], 'New secret')
Can anyone help me on this?
You could use a simple traversal function for this that just traverses the passed properties until it arrives as the final one, then sets that to the new value.
function myCustomAssignment(mainObject, propertyList, newValue) {
const lastProp = propertyList.pop();
const propertyTree = propertyList.reduce((obj, prop) => obj[prop], mainObject);
propertyTree[lastProp] = newValue;
}
You could even add propertyList = propertyList.split('.') to the top of this function so the list can be passed in as an easy-to-read string, like myCustomAssignment(mainObject, 'firstLevel.secondLevel.thirdLevel.actualProperty', 'new value') if you wanted that.
export function mutateState(mainObject: object, propertyList: string[], newValue: any) {
const lastProp = propertyList.pop();
const newState: object = { ...mainObject };
const propertyTree =
propertyList
.reduce((obj, prop) => {
obj[prop] = { ...newState[prop], ...obj[prop] };
return obj[prop];
}, newState);
propertyTree[lastProp] = newValue;
return newState as unknown;
}
This fixed my issue. thanks all..

prevent duplicate objects being added to state react redux

I have a question regarding preventing duplicates from being added to my redux store.
It should be straight forward but for some reason nothing I try is working.
export const eventReducer = (state = [], action) => {
switch(action.type) {
case "ADD_EVENT":
return [...state, action.event].filter(ev => {
if(ev.event_id !== action.event.event_id){
return ev;
}
});
default:
return state;
}
};
The action looks something like the below:
{
type: "ADD_EVENT",
event: { event_id: 1, name: "Chelsea v Arsenal" }
}
The issue is that on occasions the API I am working with is sending over identical messages through a websocket, which means that two identical events are getting added to my store.
I have taken many approaches but cannot figure out how to get this to work. I have tried many SO answers,
Why your code is failing?
Code:
return [...state, action.event].filter(ev => {
if(ev.event_id !== action.event.event_id){
return ev;
}
});
Because first you are adding the new element then filtering the same element, by this way it will never add the new value in the reducer state.
Solution:
Use #array.findIndex to check whether item already exist in array or not if not then only add the element otherwise return the same state.
Write it like this:
case "ADD_EVENT":
let index = state.findIndex(el => el.event_id == action.event.event_id);
if(index == -1)
return [...state, action.event];
return state;
You can use Array.prototype.find().
Example (Not tested)
const eventExists = (events, event) => {
return evets.find((e) => e.event_id === event.event_id);
}
export const eventReducer = (state = [], action) = > {
switch (action.type) {
case "ADD_EVENT":
if (eventExists(state, action.event)) {
return state;
} else {
return [...state, action.event];
}
default:
return state;
}
};
Update (#CodingIntrigue's comment)
You can also use Array.prototype.some() for a better approach
const eventExists = (events, event) => {
return evets.some((e) => e.event_id === event.event_id);
}
export const eventReducer = (state = [], action) = > {
switch (action.type) {
case "ADD_EVENT":
if (eventExists(state, action.event)) {
return state;
} else {
return [...state, action.event];
}
default:
return state;
}
};
Solution:
const eventReducer = ( state = [], action ) => {
switch (action.type) {
case 'ADD_EVENT':
return state.some(( { event_id } ) => event_id === action.event.event_id)
? state
: [...state, action.event];
default:
return state;
}
};
Test:
const state1 = eventReducer([], {
type: 'ADD_EVENT',
event: { event_id: 1, name: 'Chelsea v Arsenal' }
});
const state2 = eventReducer(state1, {
type: 'ADD_EVENT',
event: { event_id: 2, name: 'Chelsea v Manchester' }
});
const state3 = eventReducer(state2, {
type: 'ADD_EVENT',
event: { event_id: 1, name: 'Chelsea v Arsenal' }
});
console.log(state1, state2, state3);
You can something like this, for the logic part to ensure you don't get the same entry twice.
const x = filter.arrayOfData(item => item.event_id !== EVENT_FROM_SOCKET);
if (x.length === 0) {
// dispatch action here
} else {
// ignore and do nothing
}
You need to be careful when using Arrays in reducers. You are essentially adding more items to the list when you call:
[...state, action.event]
If you instead use a map then you can prevent duplicates
const events = { ...state.events }
events[action.event.event_id] = action.event.name]
{...state, events }
If duplicate exist in previous state then we should return same state else update the state
case "ADDPREVIEW":
let index = state.preview.findIndex(dup => dup.id == action.payload.id);
return {
...state,
preview: index == -1 ? [...state.preview,action.payload]:[...state.preview]
};

In React/Redux reducer, how can I update a string in an nested array in an immutable way?

My store looks like this,
{
name: "john",
foo: {},
arr: [
{
id:101,
desc:'comment'
},
{
id:101,
desc:'comment2'
}
]
}
My textarea looks like this
<textarea
id={arr.id} //"101"
name={`tesc:`}
value={this.props.store.desc}
onChange={this.props.onChng}
/>
My action is
export const onChng = (desc) => ({
type: Constants.SET_DESC,
payload: {
desc
}
});
My reducer
case Constants.SET_DESC:
return update(state, {
store: {
streams: {
desc: { $set: action.payload.desc }
}
}
});
It works only if arry is an object, I had to make changes to the stream to an array and I am confused how I can update to an array, also how does get the right value from the store.
The following example taken from the redux documentation might help you in the use case how to update items in an array. For more on this you can read on here http://redux.js.org/docs/recipes/StructuringReducers.html
state structure is something like this
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
and reducer code is like below
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues);
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if(item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item;
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item);
return updatedItem;
});
return updatedItems;
}
function appReducer(state = initialState, action) {
switch(action.type) {
case 'EDIT_TODO' : {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, {text : action.text});
});
return updateObject(state, {todos : newTodos});
}
default : return state;
}
}
If you have to update an element in a array within your store you have to copy the array and clone the matching element to apply your changes.
So in the first step your action should contain either the already cloned (and changed) object or the id of the object and the properties to change.
Here is a rough example:
export class MyActions {
static readonly UPDATE_ITEM = 'My.Action.UPDATE_ITEM';
static updateItem(id: string, changedValues: any) {
return { type: MyActions.UPDATE_ITEM, payload: { id, changedValues } };
}
}
export const myReducer: Reducer<IAppState> = (state: IAppState = initialState, action: AnyAction): IAppState => {
switch (action.type) {
case MyActions.UPDATE_ITEM:
return { ...state, items: merge(state.items, action.payload) };
default:
return state;
}
}
const merge = (array, change) => {
// check if an item with the id already exists
const index = array.findIndex(item => item.id === change.id);
// copy the source array
array = [...array];
if(index >= 0) {
// clone and change the existing item
const existingItem = array[index];
array[index] = { ...existingItem, ...change.changedValues };
} else {
// add a new item to the array
array.push = { id: change.id, ...change.changedValues };
}
return array;
}
To update an array, I would use immutability helper and do something like this - to your reducer
let store = {"state" : {
"data": [{
"subset": [{
"id": 1
}, {
"id": 2
}]
}, {
"subset": [{
"id": 10
}, {
"id": 11
}, {
"id": 12
}]
}]
}}
case Constants.SET_DESC:
return update(store, {
"state" : {
"data": {
[action.indexToUpdate]: {
"subset": {
$set: action.payload.desc
}
}
}
}
})
});

Keeping data object after filtering

Small Background intro
I´m currently working the User Administration page of my project and running into a small problem here. I have a table which contains some material-ui`s Usercard. For Each user that uses my System exist´s one card. The card´s are generated from data that comes from my database and then written into a redux store.
The Admin can do several interactions with the database that changes some Userdata. To provide an easy way to find a specific user a <TextField /> was implemented that filter´s the table of Usercards.
All of the things mentioned here works!
The Problem
As mentioned in the Intro the data are stored in a redux store. When I filter the data, an action is dispatched
export const FILTER_ALL_USER_BY_NAME = "FILTER_ALL_USER_BY_NAME"
export const FILTER_ALL_USER_BY_DEPARTMENT = "FILTER_ALL_USER_BY_DEPARTMENT"
export default function filterAllUser(filter, filterOption){
return (dispatch, getState) => {
if(filterOption === 'name'){
return dispatch (filterUserByName(filter))
}else{
return dispatch (filteUserByDepartment(filter))
}
}
}
function filterUserByName(filter){
return {
type: FILTER_ALL_USER_BY_NAME,
filter: filter
}
}
function filteUserByDepartment(filter){
return {
type: FILTER_ALL_USER_BY_DEPARTMENT,
filter: filter
}
}
The Reducer
Even if the reducers works, it is the main reason for my problem.
Why?
It is because, when I filter the data I was not able to really filter the state, rather then return a new state object which leads me to the problem that the allUserData and filteredUserData get out of sync after the userdata are changed.
Let me explain this in code
function allUser(state = {allUserData: []}, action){
switch (action.type){
case 'REQUEST_ALL_USER':
return Object.assign({}, state, {
isFetching: true
});
case 'RECEIVE_ALL_USER':
return Object.assign({}, state, {
isFetching: false,
allUserData: action.items
});
case 'FILTER_ALL_USER_BY_NAME':
return Object.assign({}, state, {
filteredUserData: state.allUserData.filter(user => user.userNameLast.toLowerCase().indexOf(action.filter.toLowerCase()) >= 0)
});
case 'FILTER_ALL_USER_BY_DEPARTMENT':
return Object.assign({}, state, {
filteredUserData: state.allUserData.filter(user => user.departmentName.toLowerCase().indexOf(action.filter.toLowerCase()) >= 0)
});
default:
return state
}
}
But when I´m trying to filter the original state and the user removes the filter, the data that didn´t matched the filter are gone.
case 'FILTER_ALL_USERS': return allUsers.filter(user => user.userNameLast.toLowerCase().indexOf(action.filter.toLowerCase()) >= 0);
How can I filter the state, but keep the data ?
As requested, I have put together some code for you. It would be something like below.
As a side note, I would pass the filter as { field: 'userLastName', text: your filter text} as the filter criteria. Or to make it even more scalable, you can pass a filter-handler instead of text above. That way you can have any type of filter, and not just text filters.
export default function allUser(state = {allUserData: [], filters: {}, filteredUserData: []}, action){
switch (action.type){
case 'REQUEST_ALL_USER':
return {
...state,
isFetching: true
};
case 'RECEIVE_ALL_USER':
return {
...state,
isFetching: false,
allUserData: action.items,
filteredUserData: filterData(action.items, state.filters),
};
case 'FILTER_ALL_USER_BY_NAME':
{
const updatedFilters = {
...state.filters,
userNameLast: action.filter
}
return {
...state,
filteredUserData: filterData(state.allUserData, updatedFilters),
filters: updatedFilters
};
}
case 'FILTER_ALL_USER_BY_DEPARTMENT':
{
const updatedFilters = {
...state.filters,
departmentName: action.filter
}
return {
...state,
filteredUserData: filterData(state.allUserData, updatedFilters),
filters: updatedFilters
};
}
default:
return state
}
}
const filterData = (users, filters) => {
return users.filter(filterFn(filters));
};
const filterFn = filters => item => Object.keys(filters).reduce((res, filter) => {
return res && item[filter].toLowerCase().indexOf(filters[filter].toLowerCase()) >= 0;
}, true);
Unit tests
import usersReducer from './users';
describe('usersReducer', () => {
describe('RECEIVE_ALL_USER', () => {
const RECEIVE_ALL_USER = 'RECEIVE_ALL_USER';
it('should replace users in state', () => {
const initialState = { isFetching: false, allUserData: [{ name: 'A' }], filters: {}};
const newUsers = [{ name: 'B' }];
const newState = { ...initialState, allUserData: newUsers, filteredUserData: newUsers};
expect(usersReducer(initialState, { type: RECEIVE_ALL_USER, items: newUsers })).toEqual(newState);
})
})
describe('FILTER_ALL_USER_BY_NAME', () => {
let FILTER_ALL_USER_BY_NAME = 'FILTER_ALL_USER_BY_NAME';
it('should filter users by name', () => {
const initialState = { isFetching: false, allUserData: [{ userNameLast: 'Doe' }, { userNameLast: 'Smith' }], filters: {}};
const filterText = 'd';
const finalState = { isFetching: false,
allUserData: [{ userNameLast: 'Doe' }, { userNameLast: 'Smith' }],
filters: { userNameLast: filterText },
filteredUserData: [{ userNameLast: 'Doe' }]
};
expect(usersReducer(initialState, { type: FILTER_ALL_USER_BY_NAME, filter: filterText})).toEqual(finalState);
})
})
describe('FILTER_ALL_USER_BY_DEPARTMENT', () => {
let FILTER_ALL_USER_BY_DEPARTMENT = 'FILTER_ALL_USER_BY_DEPARTMENT';
it('should filter users by department', () => {
const initialState = { isFetching: false, allUserData: [{ departmentName: 'IT' }, { departmentName: 'Human Resources' }], filters: {}};
const filterText = 'it';
const finalState = {
...initialState,
filters: { departmentName: filterText },
filteredUserData: [{ departmentName: 'IT' }]
};
expect(usersReducer(initialState, { type: FILTER_ALL_USER_BY_DEPARTMENT, filter: filterText})).toEqual(finalState);
})
})
});
Best for these cases is to some selector library, example reselect. Instead of editing the original state create selectors for sorting and filter and pass the result to component.
There's also a quite similar example in reselect documentation https://github.com/reactjs/reselect#selectorstodoselectorsjs.

Categories