React: setState with spead operator seems to modify state directly - reactjs

I am creating a page in React to filter attributes that are defined in my state with a isChecked like so:
this.state = {
countries: [
{ "id": 1, "name": "Japan", "isChecked": false },
{ "id": 2, "name": "Netherlands", "isChecked": true },
{ "id": 3, "name": "Russia", "isChecked": true }
//...
],
another: [
{ "id": 1, "name": "Example1", "isChecked": true },
{ "id": 2, "name": "Example2", "isChecked": true },
{ "id": 3, "name": "Example3", "isChecked": false }
//...
],
//... many more
};
I am creating a function resetFilters() to set all the isChecked to false in my state:
resetFilters() {
// in reality this array contains many more 'items'.
for (const stateItemName of ['countries', 'another']) {
// here i try to create a copy of the 'item'
const stateItem = [...this.state[stateItemName]];
// here i set all the "isChecked" to false.
stateItem.map( (subItem) => {
subItem.isChecked = false;
});
this.setState({ stateItemName: stateItem });
}
this.handleApiCall();
}
My problem is: it seems I am directly modifying state, something that is wrong, according to the docs. Even though my function seems to work, when I remove the line this.setState({ stateItemName: stateItem }); it will also seem to work and when I console log stateItem and this.state[stateItemName] they are always the same, even though I am using the spread operator which should create a copy. My question: how is this possible / what am I doing wrong?

That is because the spread syntax does only shallow copying. If you want to carry out deep copying, you should also be spreading the inner objects within each array.
for (const stateItemName of ['countries', 'another']) {
const stateItem = [...this.state[stateItemName]];
const items = stateItem.map( (subItem) => ({
...subItem,
isChecked: false,
}));
this.setState({ [stateItemName]: items });
}

I think your code could be reduced more so, for example an approach could be:
function resetFilters() {
const targetItems = ['countries', 'another'];
const resetState = targetItems.reduce((acc, item) => ({
...acc,
[item]: this.state[item].map(itemArray => ({
...itemArray,
isChecked: false
}))
}), {})
this.setState(state => ({ ...state, ...resetState }), this.handleApiCall);
}
The benefit here is that the api call is done after state is updated. While updating current state correctly.
Let me know how it works out 👌🏻
-Neil

Related

How can i change the value of an element in an object from extrareducer redux

This is the initial state:
const initialState ={
ordersWholesale:[
{
"id": 14,
"name": "XTPara 650mg Tablet",
"code": "XTP5656",
"date": "17/10/2022",
"accepted": null,
"wholesale": "shakthi",
"quantity": "5"
},
{
"id": 15,
"name": "Dolo 650 Tablet",
"code": "DOL1213",
"date": "17/10/2022",
"accepted": false,
"wholesale": "shakthi",
"quantity": "5"
},
],
}
This is the slice reducer
extraReducer: {
[asyncOrderAccept.fulfilled]: (state, { payload }) => {
}
}
How can I change only the value orderWholesale[0]['accepted']: true using the payload value which is 14?
If I'm understanding your question correctly that the action payload is the id of the ordersWholesale state element you want to toggle true, then you'll need to search the array to find the correct element by id and then update that element. Keep in mind that state is the ordersWholesale array and that Array.prototype.find potentially returns undefined if no match is found.
extraReducer: {
[asyncOrderAccept.fulfilled]: (state, { payload }) => {
const el = state.find(order => order.id === payload);
if (el) {
el.accepted: true,
}
},
}
This may also work for you if you can use Optional Chaining.
extraReducer: {
[asyncOrderAccept.fulfilled]: (state, { payload }) => {
state.find(order => order.id === payload)?.accepted = true;
},
}

Global state with redux somehow switches variables

I am very confused as to why this is happening as this has never happened with me before using redux. I am building a react native application and currently when I try to console log store.getStore() get the following output.
Object {
"userState": Object {
"currentUser": Object {
"email": "test120#gmail.com",
"username": "test13",
},
"listings": Array [],
},
}
Now, when I dispatch my fetchUserListings() action, which should update the listings in the state the following happens.
Object {
"userState": Object {
"currentUser": Array [
Object {
"addressData": Object {
"description": "2300 Yonge Street, Toronto, ON, Canada",
"matched_substrings": Array [
Object {
"length": 3,
"offset": 0,
},
],
"place_id": "ChIJx4IytjwzK4gRwIPk2mqEJow",
"reference": "ChIJx4IytjwzK4gRwIPk2mqEJow",
"structured_formatting": Object {
"main_text": "2300 Yonge Street",
"main_text_matched_substrings": Array [
Object {
"length": 3,
"offset": 0,
},
],
"secondary_text": "Toronto, ON, Canada",
},
"terms": Array [
Object {
"offset": 0,
"value": "2300",
},
Object {
"offset": 5,
"value": "Yonge Street",
},
Object {
"offset": 19,
"value": "Toronto",
},
Object {
"offset": 28,
"value": "ON",
},
Object {
"offset": 32,
"value": "Canada",
},
],
"types": Array [
"street_address",
"geocode",
],
},
"addressDescription": "2300 Yonge Street, Toronto, ON, Canada",
"bath": "6",
"benefits": Array [
"Large Beds",
"Nearby Bustop",
"In-building gym",
],
"urls": Array [
"https://firebasestorage.googleapis.com/v0/b/studenthousingfinder-11f55.appspot.com/o/listing%2FoHr0OMukEFguYxJborrvMAJQmre2%2F0.bd7cwka5gj?alt=media&token=81b3e06a-65a9-44a7-a32d-d328014058e7",
"https://firebasestorage.googleapis.com/v0/b/studenthousingfinder-11f55.appspot.com/o/listing%2FoHr0OMukEFguYxJborrvMAJQmre2%2F0.k78etqzypk?alt=media&token=e2622547-00f4-447b-8bea-799758734f0d",
],
},
],
"listings": Array [],
},
}
Basically the API call is working and the state is updated, however somehow the data sent back is updating the currentUser in the state rather than the listings.
Here is my current reducer code:
import {USER_LISTINGS_STATE_CHANGE, USER_STATE_CHANGE} from '../constants';
const initialState = {
currentUser: null,
listings: [],
};
export const userReducer = (state = state || initialState, action) => {
switch (action.type) {
case USER_STATE_CHANGE:
return {
listings: state.listings,
currentUser: action.payload,
};
case USER_LISTINGS_STATE_CHANGE:
return {
currentUser: state.currentUser,
listings: action.payload,
};
default:
return state;
}
};
and here are the 2 functions I use to make the API request
export function fetchUser() {
return async (dispatch) => {
db.collection('users')
.doc(auth.currentUser.uid)
.get()
.then((snapshot) => {
if (snapshot.exists) {
console.log('Yo');
dispatch({type: USER_STATE_CHANGE, payload: snapshot.data()});
} else {
console.log('Does not exist');
}
});
};
}
export function fetchUserListings() {
return async (dispatch) => {
db.collection('posts')
.doc(auth.currentUser.uid)
.collection('userListings')
.orderBy('title', 'desc')
.get()
.then((snapshot) => {
let listingArr = snapshot.docs.map((doc) => {
const data = doc.data();
const id = doc.id;
return {id, ...data};
});
dispatch({
type: USER_LISTINGS_STATE_CHANGE,
payload: listingArr,
});
});
};
}
Any help would be appreciated as I'm really lost as to why this is happening!
It seems like that your users collection in database might be having wrong entry.
Fact that brought me to this conclusion is that in your fetchUserListings() function, you're actually adding an extra field to json object i.e id.
Now, the console.log output doesn't contain this id field which could only mean that fetchUserListings() is not the one being called here.
You can try putting some try catch block and console.log('fetchUser', snapshot.data()) & console.log('fetchUserListings', snapshot.docs) in respective functions to see which one is being called.

How can I groupby in React/Redux code (new to frontend)?

I am having difficulty with this function to run in my React/Redux code. I am fairly new to React (mostly work on backend) so I am not sure why it's not running as expected. Is it in the right place? I am also having trouble finding where console.log is printing in the console, since the console is set up with prev action, action, next state patterns....
I have this function defined in my 'actions.js' (where it is also called later on):
const groupBy = (array, key) => {
return array.reduce((result, currentValue) => {(
result[currentValue[key]] = result[currentValue[key]] || []).push(
currentValue
);
return result;
}, {});
};
Here is where the function is called (same file):
export function getAlerts() {
return dispatch => {
api.get('notifications', (response) => {
const grouped = groupBy(response.results.data, 'email');
dispatch(updateFetchedAlerts(grouped));
}, (error) => {
console.warn(error);
})
}
}
The input, response.results.data, looks something like this:
[{"email": test#email.com, "is_active": true, "alert_id": 1, "pk": 1},
{"email": test#email.com, "is_active": true, "alert_id": 2, "pk": 2},
{"email": different#email.com, "is_active": true, "alert_id": 1, "pk": 3}]
I want it to look like this:
[{"test#email.com": [{"is_active": true, "alert_id": 1, "pk": 1},
{"is_active": true, "alert_id": 2, "pk": 2}],
"different#email.com": [{"is_active": true, "alert_id": 1, "pk": 3}]}]
but it seems to not be running this function, I've rerun yarn build and used incognito...
UPDATE: This function actually WORKS! Thanks all. The redux developer tools are very helpful. Now the second problem is I need to add in my own keys.... So ideally the result would look like this. Preferably no lodash!:
[{"email": "test#email.com",
"alerts": [{"is_active": true, "alert_id": 1, "pk": 1},
{"is_active": true, "alert_id": 2, "pk": 2}]},
{"email": "different#email.com",
"alerts": [{"is_active": true, "alert_id": 1, "pk": 3}]}]
You can take the same function you used to group by which yields object of shape
{ [string]: object[] }
Using Object.entries allows you to convert this to an array of key-value pairs that you can map to an array of objects with shape
{
email: string,
alerts: object[],
}
Function updates
const groupBy = (array, key) =>
array.reduce((result, { [key]: k, ...rest }) => {
(result[k] = result[k] || []).push(rest);
return result;
}, {});
const groupDataBy = (array, key) =>
Object.entries(groupBy(array, key)).map(([email, alerts]) => ({
email,
alerts
}));
The map function callback ([email, alerts]) => ({ email, alerts }) uses array destructuring to assign the array of [key, value] to named variables email and alerts, and object shorthand notation to create an object with keys named for the variables.
const data = [{
"email": "test#email.com",
"is_active": true,
"alert_id": 1,
"pk": 1
},
{
"email": "test#email.com",
"is_active": true,
"alert_id": 2,
"pk": 2
},
{
"email": "different#email.com",
"is_active": true,
"alert_id": 1,
"pk": 3
}
];
const groupBy = (array, key) =>
array.reduce((result, { [key]: k, ...rest }) => {
(result[k] = result[k] || []).push(rest);
return result;
}, {});
const groupDataBy = (array, key) =>
Object.entries(groupBy(array, key)).map(([email, alerts]) => ({
email,
alerts
}));
const res = groupDataBy(data, 'email');
console.log(res)
Ok, first the console is in the Browser Developer Tools seccion console. There you can view all the console.log. In Chrome you can open it by pressing F12.
And the other question, the function that you`re needing is:
//Function groupBy.
function groupBy(array, key) {
let arrayReduced = array.reduce(
(result, { [key]: k, ...rest }) => {
(result[k] = result[k] || []).push(rest);
return result;
},
{}
);
return arrayReduced;
}
//Your example data.
const group = [
{
email: 'test#email.com',
is_active: true,
alert_id: 1,
pk: 1,
},
{
email: 'test#email.com',
is_active: true,
alert_id: 2,
pk: 2,
},
{
email: 'different#email.com',
is_active: true,
alert_id: 1,
pk: 3,
},
];
//Function executed
const result = groupBy(group, 'email');
//Result
console.log(result);
I hope it helps!

how to filter through an array and check if a nested object includes a value of true using reselect in redux

I'm new to reselect and I've been having success with creating selectors so far until I tried to filter a list by a nested key value pair. Most of the options are strings or array of strings and they are working just fine, but I can't figure out how to filters this list by the true/false values in an object.
here is my working selector so far and the json I'm using it to sort
const getItemsVisibilityFilter = state => state.itemsVisibilityFilter
const getItems = state => state.items
export const getVisibleItems = createSelector(
[getItemsVisibilityFilter, getItems],
(itemsVisibilityFilter, items) => {
switch (itemsVisibilityFilter) {
case 'SHOW_ALL':
console.log(items)
return items
case 'SHOW_DAMAGE':
return items.filter(item => item.tags.includes('Damage'))
case 'SHOW_ATTACK_SPEED':
return items.filter(item => item.tags.includes('AttackSpeed'))
case 'SHOW_LIFE_STEAL':
return items.filter(item => item.tags.includes('LifeSteal'))
default:
return items
}
}
)
and this is a single item from the JSON
"1036": {
"stats": {
"FlatPhysicalDamageMod": 10
},
"description": "<stats>+10 Attack Damage</stats>",
"gold": {
"total": 350,
"sell": 245,
"base": 350,
"purchasable": true
},
"tags": [
"Damage",
"Lane"
],
"plaintext": "Slightly increases Attack Damage",
"image": {
"full": "1036.png",
"group": "item",
"sprite": "item0.png",
"h": 48,
"w": 48,
"y": 48,
"x": 48
},
"sanitizedDescription": "+10 Attack Damage",
"maps": {
"8": true,
"10": true,
"11": true,
"12": true,
"14": false,
"16": false,
"18": true,
"19": true
},
"into": [
"3077",
"3123",
"1053",
"3155",
"3134",
"3133",
"3034",
"3035",
"3044",
"3052",
"3072",
"3122",
"3144",
"3252"
],
"id": 1036,
"name": "Long Sword"
},
my question is how do I filter through the "maps" object and return the items that have a value of true? If this helps. what I would like to add to the original selector is 'SHOW_SUMMONERS_RIFT' - "maps": {11: true} like so
const getItemsVisibilityFilter = state => state.itemsVisibilityFilter
const getItems = state => state.items
export const getVisibleItems = createSelector(
[getItemsVisibilityFilter, getItems],
(itemsVisibilityFilter, items) => {
switch (itemsVisibilityFilter) {
case 'SHOW_ALL':
console.log(items)
return items
case 'SHOW_DAMAGE':
return items.filter(item => item.tags.includes('Damage'))
case 'SHOW_ATTACK_SPEED':
return items.filter(item => item.tags.includes('AttackSpeed'))
case 'SHOW_LIFE_STEAL':
return items.filter(item => item.tags.includes('LifeSteal'))
case 'SHOW_SUMMONERS_RIFT':
return items.filter(item => item.maps.includes(I don't know what or to put here to see if 11 === true))
default:
return items
}
}
)
If this isn't enough code to help provide a solution. please let me know and I can post anything you think would be more relevant. Or if there are docs somewhere I should be reading more... Everything I have found though is about updating nested objects and I can't find anything about comparing values. please and thank you
SOLUTION --- thanks to Hardik Modha for putting me on the right path. I don't know if this is the best solution or even a good way for using Reselect, Redux, or even plain javascript haha, but here is the case I came up with and it works for the selector based on a nested object
case 'SHOW_SUMMONERS_RIFT':
const riftItems = items.filter(item => {
const mapKey = Object.keys(item.maps)
const mapValue = Object.values(item.maps)
if (mapKey[2] && mapValue[2] === true) {
return item
}
return null
})
return riftItems
You just need to get all entries first using Object.keys
and then filter the entries which are having value true using filter
const filtered = Object.keys(map.maps).filter((item) => map.maps[item]);
const myObject = {
"1036": {
"maps": {
"8": true,
"10": true,
"11": true,
"12": true,
"14": false,
"16": false,
"18": true,
"19": true
},
},
"1037": {
"maps": {
"8": false,
"10": true,
"11": true,
"12": false,
"14": false,
"16": false,
"18": true,
"19": true
},
},
"1038": {
"maps": {
"8": true,
"10": false,
"11": true,
"12": true,
"14": false,
"16": false,
"18": false,
"19": true
},
},
}
Object.keys(myObject).forEach((key) => {
const maps = myObject[key].maps;
console.log(Object.keys(maps).filter(item => maps[item]));
});
You would need to manipulate your 1036.maps...
using Object.entries()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
Example from docs.
const obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.entries(obj)); // [ ['0', 'a'], ['1', 'b'], ['2', 'c'] ]
Example for you...
let workableData = Object.entries(1036.maps)
workableData.map(ele => { ele[1] === true ? do this : do this })

How can I update an array in state?

When my app loads I generate some initial data. My initial state is:
const INITIAL_STATE = { calendarData: [ ], index: 0 }
The data generated is an array of objects:
[
{
"date": "2017-09-24T16:13:24.419Z",
"id": 0,
"swiped": false,
"uri": "http://www.someurl.com/24.png",
},
{
"date": "2017-09-25T16:13:24.426Z",
"id": 1,
"swiped": false,
"uri": "http://www.someurl.com/25.png",
},
{
"date": "2017-09-26T16:13:24.426Z",
"id": 2,
"swiped": false,
"uri": "http://www.someurl.com/26.png",
}
]
I'm attempting to update calendarData like so:
return {...state, calendarData: [...state.calendarData, ...aboveArray]};
However, when I log mapStateToProps
const mapStateToProps = state => {
return { calendarData: state.calendarData };
};
Console shows the following:
Object {
"calendarData": Object {
"calendarData": Array [],
"index": 0,
},
I am trying to update the empty calendarData array in INITIAL_STATE with the new array of objects.
Before I made this change I was initializing state = [] and returning the aboveArray, which worked. Now I want my state to be an object that has a calendarData array and an index key,
state = {
calendarData: [],
index: 0,
}

Resources