Related
I have these two states that consist in two arrays.
const bundle = [
{
id: 1,
type: "schedule",
action: "skip",
target_action: "reset"
},
{
id: 2,
type: "schedule",
action: "reset",
target_action: "skip"
},
{
id: 1,
type: "check",
action: "reset",
target_action: "skip"
},
{
id: 2,
type: "check",
action: "skip",
target_action: "reset"
}
];
const active = [
{
id: 1,
type: "schedule",
isActive: true
},
{
id: 2,
type: "schedule",
isActive: false
},
{
id: 1,
type: "check",
isActive: true
},
{
id: 2,
type: "check",
isActive: false
}
];
When items in active turns inactive (isActive: false) by clicking a button, they get filtered out of the array.
const handleActive = (item) => {
setActive((prevState) => {
const existingItem = prevState.find(
(activeItem) =>
activeItem.id === bundleItem.id &&
activeItem.type === bundleItem.type,
);
if (!existingItem) {
return [...active, { ...bundleItem, isActive: true }];
}
return prevState
.map((oldItem) => {
return oldItem.id === existingItem.id &&
oldItem.type === bundleItem.type
? { ...existingItem, isActive: !oldItem.isActive }
: oldItem;
})
.filter((itemToFilter) => itemToFilter.isActive);
});
};
Basically, I want to implement a useEffect that dynamically updates bundle in two ways simultaneously:
items must have at least one of action or c_action keys
when active gets updated (some elements get inactive and filtered out), I want to keep only the common items between the two arrays (same ID and type)
I implemented these two effects.
The first one to filter out the inactive elements from bundle:
React.useEffect(() => {
setBundle((prevState) => {
return bundle.filter((bundleItem) =>
active.some(
(activeItem) =>
activeItem.id === bundleItem.id &&
activeItem.type === bundleItem.type,
),
);
})
}, [active]);
The other one to filter out from bundle elements that doesn't "action" or "c_action" key.
React.useEffect(() => {
setBundle((prevState) => {
return bundle.filter(
(bundleItem) => bundleItem.action || bundleItem.c_action
);
});
}, [bundle]);
The second useEffect I implemented throws an infinite loop: bundle gets endlessly updated.
Thanks, a lot.
It seems to me that what you're looking for is actually an useMemo use case
you can do something like
const filteredBundle = useMemo(()=> bundle.filter(
(bundleItem) => bundleItem.action || bundleItem.c_action
),[bundle]);
And use the filtered bundle where makes sense
Data Structure coming back from the server
[
{
id: 1,
type: "Pickup",
items: [
{
id: 1,
description: "Item 1"
}
]
},
{
id: 2,
type: "Drop",
items: [
{
id: 0,
description: "Item 0"
}
]
},
{
id: 3,
type: "Drop",
items: [
{
id: 1,
description: "Item 1"
},
{
id: 2,
description: "Item 2"
}
]
},
{
id: 0,
type: "Pickup",
items: [
{
id: 0,
description: "Item 0"
},
{
id: 2,
description: "Item 2"
}
]
}
];
Each element represents an event.
Each event is only a pickup or drop.
Each event can have one or more items.
Initial State
On initial load, loop over the response coming from the server and add an extra property called isSelected to each event, each item, and set it as false as default. -- Done.
This isSelected property is for UI purpose only and tells user(s) which event(s) and/or item(s) has/have been selected.
// shove the response coming from the server here and add extra property called isSelected and set it to default value (false)
const initialState = {
events: []
}
moveEvent method:
const moveEvent = ({ events }, selectedEventId) => {
// de-dupe selected items
const selectedItemIds = {};
// grab and find the selected event by id
let foundSelectedEvent = events.find(event => event.id === selectedEventId);
// update the found event and all its items' isSelected property to true
foundSelectedEvent = {
...foundSelectedEvent,
isSelected: true,
items: foundSelectedEvent.items.map(item => {
item = { ...item, isSelected: true };
// Keep track of the selected items to update the other events.
selectedItemIds[item.id] = item.id;
return item;
})
};
events = events.map(event => {
// update events array to have the found selected event
if(event.id === foundSelectedEvent.id) {
return foundSelectedEvent;
}
// Loop over the rest of the non selected events
event.items = event.items.map(item => {
// if the same item exists in the selected event's items, then set item's isSelected to true.
const foundItem = selectedItemIds[item.id];
// foundItem is the id of an item, so 0 is valid
if(foundItem >= 0) {
return { ...item, isSelected: true };
}
return item;
});
const itemCount = event.items.length;
const selectedItemCount = event.items.filter(item => item.isSelected).length;
// If all items in the event are set to isSelected true, then mark the event to isSelected true as well.
if(itemCount === selectedItemCount) {
event = { ...event, isSelected: true };
}
return event;
});
return { events }
}
Personally, I don't like the way I've implemented the moveEvent method, and it seems like an imperative approach even though I'm using find, filter, and map.
All this moveEvent method is doing is flipping the isSelected flag.
Is there a better solution?
Is there a way to reduce the amount of looping? Maybe events should be an object and even its items. At least, the lookup would be fast for finding an event, and I don't have to use Array.find initially. However, I still have to either loop over each other non selected events' properties or convert them back and forth using Object.entries and/or Object.values.
Is there more a functional approach? Can recursion resolve this?
Usage and Result
// found the event with id 0
const newState = moveEvent(initialState, 0);
// Expected results
[
{
id: 1,
type: 'Pickup',
isSelected: false,
items: [ { id: 1, isSelected: false, description: 'Item 1' } ]
}
{
id: 2,
type: 'Drop',
// becasue all items' isSelected properties are set to true (even though it is just one), then set this event's isSelected to true
isSelected: true,
// set this to true because event id 0 has the same item (id 1)
items: [ { id: 0, isSelected: true, description: 'Item 0' } ]
}
{
id: 3,
type: 'Drop',
// since all items' isSelected properties are not set to true, then this should remain false.
isSelected: false,
items: [
{ id: 1, isSelected: false, description: 'Item 1' },
// set this to true because event id 0 has the same item (id 2)
{ id: 2, isSelected: true, description: 'Item 2' }
]
}
{
id: 0,
type: 'Pickup',
// set isSelected to true because the selected event id is 0
isSelected: true,
items: [
// since this belongs to the selected event id of 0, then set all items' isSelected to true
{ id: 0, isSelected: true, description: 'Item 0' },
{ id: 2, isSelected: true, description: 'Item 2' }
]
}
]
One of the problems with the current solution is data duplication. You are basically trying to keep the data between the different items in sync. Instead of changing all items with the same id, make sure there are no duplicate items by using an approach closer to what you would find in a rational database.
Let's first normalize the data:
const response = [...]; // data returned by the server
let data = { eventIds: [], events: {}, items: {} };
for (const {id, items, ...event} of response) {
data.eventIds.push(id);
data.events[id] = event;
event.items = [];
for (const {id, ...item} of items) {
event.items.push(id);
data.items[id] = item;
}
}
This should result in:
const data {
eventIds: [1, 2, 3, 0], // original order
events: {
0: { type: "Pickup", items: [0, 2] },
1: { type: "Pickup", items: [1] },
2: { type: "Drop", items: [0] },
3: { type: "Drop", items: [1, 2] },
},
items: {
0: { description: "Item 0" },
1: { description: "Item 1" },
2: { description: "Item 2" },
},
};
The next thing to realize is that the isSelected property of an event is computed based on the isSelected property of its items. Storing this would mean more data duplication. Instead calculate it though a function.
const response = [{id:1,type:"Pickup",items:[{id:1,description:"Item 1"}]},{id:2,type:"Drop",items:[{id:0,description:"Item 0"}]},{id:3,type:"Drop",items:[{id:1,description:"Item 1"},{id:2,description:"Item 2"}]},{id:0,type:"Pickup",items:[{id:0,description:"Item 0"},{id:2,description:"Item 2"}]}];
// normalize incoming data
let data = { eventIds: [], events: {}, items: {} };
for (const {id, items, ...event} of response) {
data.eventIds.push(id);
data.events[id] = event;
event.items = [];
for (const {id, ...item} of items) {
event.items.push(id);
data.items[id] = item;
item.isSelected = false;
}
}
// don't copy isSelected into the event, calculate it with a function
const isEventSelected = ({events, items}, eventId) => {
return events[eventId].items.every(id => items[id].isSelected);
};
// log initial data
console.log(data);
for (const id of data.eventIds) {
console.log(`event ${id} selected?`, isEventSelected(data, id));
}
// moveEvent implementation with the normalized structure
const moveEvent = (data, eventId) => {
let { events, items } = data;
for (const id of events[eventId].items) {
items = {...items, [id]: {...items[id], isSelected: true}};
}
return { ...data, items };
};
data = moveEvent(data, 0);
// log after data applying `moveEvent(data, 0)`
console.log(data);
for (const id of data.eventIds) {
console.log(`event ${id} selected? `, isEventSelected(data, id));
}
// optional: convert structure back (if you still need it)
const convert = (data) => {
const { eventIds, events, items } = data;
return eventIds.map(id => ({
id,
...events[id],
isSelected: isEventSelected(data, id),
items: events[id].items.map(id => ({id, ...items[id]}))
}));
};
console.log(convert(data));
Check browser console, for better ouput readability.
I'm not sure if this answers solves your entire problem, but I hope you got something useful info out of it.
I have an array in my state :
projects: [
{ title: 'todo 1', person: 'Sam', status: 'ongoing'},
{ title: 'project', person: 'Jack', status: 'complete' },
{ title: 'Design video', person: 'Tim', status: 'complete' },
{ title: 'Create a forum', person: 'Jade', status: 'overdue' },
{ title: 'application', person: 'Jade', status: 'ongoing'},],
From this array (projects), I would like to generate a new array with Javascript and to get this result :
totalByPersonAndStatus : [
{person : 'Sam', complete: 0, ongoing: 1, overdue: 0 },
{person : 'Jack', complete: 1, ongoing: 0, overdue: 0 },
{person : 'Tim', complete: 1, ongoing: 0, overdue: 0 },
{person : 'Jade', complete: 0, ongoing: 1, overdue: 1 },]
I tried it
totalProjectsByPersonAndStatus: state => {
state.projects.forEach(name => {
state. totalByPersonAndStatus["name"] = name.person;
});
return state. totalByPersonAndStatus;
The problem, if a make a console.log(this.totalByPersonAndStatus) I have an object with only the data of projects.name [name: "Jade", __ob__: Observer]
Can you help me ?
Thank you
You can use reduce
let projects =[{title:'todo1',person:'Sam',status:'ongoing'},{title:'project',person:'Jack',status:'complete'},{title:'Designvideo',person:'Tim',status:'complete'},{title:'Createaforum',person:'Jade',status:'overdue'},{title:'application',person:'Jade',status:'ongoing'},]
let desired = projects.reduce((output,{person,status}) => {
if( output[person] ){
output[person][status]++
} else {
output[person] = {
person,
complete: Number(status==='complete'),
ongoing: Number(status==='ongoing'),
overdue: Number(status==='overdue')
}
}
return output;
},{})
console.log(Object.values(desired))
Create a new Set for people and statuses by iterating through the projects, a set has only unique values so sets are a convenience, iterate through your people set creating a new object with all the statuses initialized to 0, then iterate over the projects to increment the various statuses that apply. This method allows any number of new statuses to be added without changing the code - dynamic.
var people = new Set();
var status = new Set();
projects.forEach((p)=>{
people.add(p.person);
status.add(p.status);
});
var totalByPersonAndStatus = [];
people.forEach((person)=>{
let peeps = { "person": person };
status.forEach((stat)=>{
peeps[stat] = 0;
});
projects.forEach((project)=>{
if (project.person === person) { peeps[project.status]++; }
});
totalByPersonAndStatus.push(peeps);
});
You could use reduce and destructuring like this:
const projects=[{title:'todo 1',person:'Sam',status:'ongoing'},{title:'project',person:'Jack',status:'complete'},{title:'Design video',person:'Tim',status:'complete'},{title:'Create a forum',person:'Jade',status:'overdue'},{title:'application',person:'Jade',status:'ongoing'}]
const merged = projects.reduce((acc,{person,status})=>{
acc[person] = acc[person] || { person, ongoing:0, complete:0, overdue:0}
acc[person][status]++;
return acc;
},{})
console.log(Object.values(merged))
The goal is create an object merged with each person as key and then increment based on the statuses:
{
"Sam": {
"person": "Sam",
"ongoing": 1,
"complete": 0,
"overdue": 0
},
"Jack": {
}
...
}
Then use Object.values, to get the final array.
You could make it a one-liner:
const projects=[{title:'todo 1',person:'Sam',status:'ongoing'},{title:'project',person:'Jack',status:'complete'},{title:'Design video',person:'Tim',status:'complete'},{title:'Create a forum',person:'Jade',status:'overdue'},{title:'application',person:'Jade',status:'ongoing'}],
output = Object.values(projects.reduce((a,{person,status})=>
((a[person] = a[person] || {person,ongoing:0,complete:0,overdue:0})[status]++,a),{}))
console.log(output)
I am new to the react-redux.
const initialState = {
Low: [
{
id: 0,
type: '',
count: '',
allowded: 6,
level: 'EASY'
}
],
Medium: [
{
id: 0,
type: '',
count: '',
allowded: 7,
level: 'MEDIUM'
}
],
High: [
{
id: 0,
type: '',
count: '',
allowded: 7,
level: 'TOUGH'
}
]
}
This is my initial state value.
After that ,
onChange(event, tobeupdated, id, type, noc, data) {
let newData = { ...this.props.data };
if (newData) {
let data = newData[type].map((object, index) => {
if (object.id === id) {
object[tobeupdated] = event.target.value;
const tobeData = newData[type];
this.props.updateLowLevel({tobeData, type}).then(() => {
let criteria_filled = this.disableAddbutton({ ...this.props.data }, type);
addedRow = `new${type}RowAdded`;
this.setState({
[addedRow]: criteria_filled ? true : false
})
});
}
This way I am updating the object values. and then replacing that whole object.
return (dispatch) => {
dispatch({
type: QUIZ_DATA,
data: tobeUpdated,
});
return Promise.resolve();
}
}
In my reducer ,
case QUIZ_DATA:
return {
...state,
[action.data.type]: [action.data.tobeData],
error: false,
}
Now,Here what is happening when I change the lets say type then it adds that array of object to as a children to the previous array. so, it gets added as many as you are adding.
SO, because of that I am not able to get that render properly.
So what Happens is ,
Low :
This way gets added. So, How Can I do this ?
Can any one helm me with this ?
Try this out as a reducer. It spreads the content of tobeData to an array.
case QUIZ_DATA:
return {
...state,
[action.data.type]: [...action.data.tobeData], // the difference is here
error: false,
}
[action.data.type]: [...action.data.tobeData],
The data you passing through action.data.tobeData (eg: [1,2,3]) is the array itself.
So when you use [action.data.type]: [action.data.tobeData], this will create the array of array [[1, 2, 3]].
You could use [action.data.type]: [...action.data.tobeData], this is called spread operator, all it's doing is basically copy all the element inside action.data.tobeData and spread it out.
You can refer to this document
Spread syntax
I've been working on a React/Redux application for building a quote. A gross simplification of my state would look something like this:
{
account: { name: 'john doe' },
lineItems:[
{ product: {id: 123, ...}, price: 10, units: 5 },
{ product: {id: 124, ...}, price: 10, units: 5 },
],
modifiers: { couponCode: 'asdf', vip: true }
}
and my reducers would be sliced something like this:
const appReducer = combineReducers<GlobalState>({
account: accountReducer,
lineItems: lineItemReducer,
modifiers: modifersReducer,
});
I've just recently gotten a requirements where I would essentially need to be able to render the entire app multiple times on a single page (basically show 1 or more quotes for different accounts on a single page). So a single state would now need to look something like this:
{
quotes: {
"0": {
account: { name: 'john doe' },
lineItems:[
{ product: {id: 123, ...}, price: 10, units: 5 },
{ product: {id: 124, ...}, price: 10, units: 5 },
],
modifiers: { couponCode: 'asdf', vip: true }
},
"1": {
account: { name: 'billy jean' },
lineItems:[
{ product: {id: 123, ...}, price: 10, units: 5 },
],
modifiers: { couponCode: '', vip: false }
},
}
}
But obviously this new state shape doesn't really work with how I've sliced my reducers. Also, seems like I'd have to refactor all my actions so that I know which quote they should be operating on? For example, if I had an action like this:
{
type: 'UPDATE_PRICE'
payload: { productId: 123, newPrice: 15 }
}
Seems like the product 123 on both quotes would be updated.
Maybe there is instead some way I can just render the entire app on the page without having to refactor my entire state? I'm not sure what my best approach would be that wouldn't requirement me to rewrite large portions of the app.
This should give you the idea. It's basically using one reducer inside another one. As simple as using a function within another function body. You can run it on runkit.com as well.
const { createStore, combineReducers } from 'redux';
const UPDATE_ACCOUNT = 'app/updat-account';
const ADD_QUOTE = 'quote/add-quote';
const appActions = {
updateAcount: (q_id, a) => ({ type: UPDATE_ACCOUNT, payload: { q_id, name: a }}),
};
const quoteActions = {
addQuote: q_id => ({ type: ADD_QUOTE, payload: q_id }),
};
const accountReducer = (app = {}, action) => {
const { type, payload } = action;
switch (type) {
case UPDATE_ACCOUNT:
return { ...app, name: payload.name }
default:
return app;
}
};
const appReducer = combineReducers({
account: accountReducer,
lineItems: (app ={}, action) => app, // just a placeholder
modifiers: (app ={}, action) => app, // just a placeholder
});
const quoteReducer = (state = {}, action) => {
const { type, payload } = action;
switch (type) {
case ADD_QUOTE:
return { ...state, [payload]: {} };
case UPDATE_ACCOUNT: {
const app = state[payload.q_id];
return app
? { ...state, [payload.q_id]: appReducer(state[payload.q_id], action) }
: state;
}
default:
return state;
}
}
const store = createStore(quoteReducer);
store.dispatch(quoteActions.addQuote(3));
store.dispatch(quoteActions.addQuote(2));
store.dispatch(appActions.updateAcount(3, 'apple'));
store.dispatch(appActions.updateAcount(4, 'orange')); // non-existent quote
store.getState():
/**
{
"2": {},
"3": {
"account": {
"name": "apple"
},
"lineItems": {},
"modifiers": {}
}
}
*/
Just wanted to add my specific answer here..
Basically I added a new root reducer as norbertpy suggested. However, I also had to add a parameter quoteId to each action to specify which quote the action originated from and should operate on. This was the most time consuming part of the refactor as now each component that dispatches actions must have access to the quote key.
Reducer
const quoteReducer = combineReducers({
account: accountReducer,
lineItems: lineItemReducer,
modifiers: modifersReducer,
});
const rootReducer = (state = {quotes: []}, action) => {
const newQuoteState = quoteReducer(state.quotes[action.quoteId], action);
const newQuotes = {...state.quotes};
newQuotes[action.quoteId] = newQuoteState;
return {...state, ...{quotes: newQuotes}};
};
Action
{
type: 'UPDATE_PRICE'
quoteId: '0',
payload: { productId: 123, newPrice: 15 }
}