How is it possible that dynamic components overwrite each other's states? - javascript

I have created dynamic Room components which are created based on a room object dispatched by Redux.
{
rooms && rooms.map((room, index) => {
const { name, temperature, humidity, timestamp } = room
return (
<Col key={index} xs={12} md={6}>
<Room
index={index}
name={name}
temperature={temperature}
humidity={humidity}
/>
</Col>
)
})
}
The details of each room are mounted properly. I create a function to maintain 10 objects in an array. However, when the array is passed into Rechart, it seems that my components are updating on top of the same state.
class Room extends Component {
linechart = () => {
const { timestamp, temperature, humidity, name } = this.props
const { chartData } = this.state
if(chartData.length > 9) chartData.shift()
chartData.push({
name,
timestamp: moment(timestamp).format('mm:ss'),
temperature,
humidity})
}
}
As you can see, the component details are displayed properly. However, the values of chartData are being stored in the same state, despite being unique components.
I ran the function with an interval of 1 second, and the logs show that the state is being updated in 0.5 second intervals. That means both <Room/> components are using the same <LineChart/> component.
Does anyone know how to overcome this issue?

In order to update items in an array, I'd recommend using the spread syntax. Spread syntax works by shallow copying the array, which keeps a reference to the original array, but allows you to overwrite any data types stored within it. This keeps the array immutable.
I'm not sure where you're getting your data from, but since you have a finite amount of rooms, then the array should already be structured like so:
data: [
{
name: "Room1",
humidity: 11,
tempature: 30,
timestamp: "Sat May 25 2019 22:23:06 GMT-0700",
},
{
name: "Room2",
humidity: 11,
tempature: 25,
timestamp: "Sat May 25 2019 22:23:06 GMT-0700",
},
...etc
]
Then you can simply store this to state and when needed map over the array and update its properties with incoming data. However, this assumes incoming data contains all the rooms. If you just need to update a specific room within the rooms array, then you can compare an id (or something that uniquely identifies it) to an incoming new data id.
For example, a new update comes in, but it's only updating Room1, then we could do something like so..
incoming data
data: [
{
name: "Room1",
humidity: 11,
tempature: 30,
timestamp: "Sat May 25 2019 22:23:06 GMT-0700",
}
];
current data stored in state as "rooms"
this.setState(prevState => ({
...prevState, // spread out any previous state not related to "rooms"
rooms: prevState.rooms.map(room => { // map over "rooms" and pull out each "room" object
return room.name === data[0].name // if the "room.name" matches incoming data's "name", then...
? { ...data[0] } // spread and overwrite it with incoming data
: room; // else leave the "room" as is
})
}));
Using array prototypes like pop and shift mutate the original array stored within state and React doesn't handle mutations to its state. You can, however, clone the room array with Array.from() or simply create a new array instance and mutate this new instance with some array prototype functions, but you must then re-set this new instance array to state to overwrite the old array -- React handles this without a problem. It's just not as clean as the spread syntax.
Working example includes both spread syntax and Array.prototype options with a setInterval:
Working example of randomly updating one item within an array by an id with a setInterval:

Related

React useState behaviour , questions on the need of a spread (....) operator

why cant I return expenses1 without spreading it in an array? since expenses1 which is the previous data is an array containing an object? from what i can see, {listOfExpenses} takes in an array of objects which is === expenses1
import Expenses from "./components/Expenses/Expenses";
import NewExpenses from "./components/NewExpenses/NewExpenses";
import React, { useState } from "react";
const expenses = [
{ id: "e1", title: "Toilet Paper", amount: 94.12, date: new Date(2020, 7, 14),},
{ id: "e2", title: "New TV", amount: 799.49, date: new Date(2021, 2, 12), },
{ id: "e3", title: "Car Insurance", amount: 294.67, date: new Date(2021, 2, 28),},
{ id: "e4", title: "New Desk (Wooden)", amount: 450, date: new Date(2021, 5, 12), },
];
function App() {
const [listOfExpenses, setListOfExpenses] = useState(expenses);
const addExpenseHandler = (expenseData) => {
setListOfExpenses((expenses1) => {
console.log(expenses1);
expenses1.push(expenseData);
console.log(...expenses1);
return [...expenses1];
});
};
return (
<div>
<NewExpenses onAddExpense={addExpenseHandler} />
<Expenses expenses={listOfExpenses} />
</div>
);
}
So I've tried console logging and compare the data difference between expenses1 and listOfExpenses, they are both the same data type thus why the need of spreading in an array?
You should never mutate data structures in React (or Redux or RxJs etc.)
The .push() method will mutate the existing array. This means it will update all references to that array, including the one internally uses to keep track of what needs to be updated. Since the old array and new array are equal, react won't update.
Instead, you should always make a new array. While the spread operator, which will copy the array will technically work, it's only because the equality operator in JS is not very good. Do do it properly use:
setListOfExpenses(expenses=>[...expenses, expenseData])
This will create a new array that starts with the original expenses, then adds the expenseData to the end. This will never mutate the original array, so properly update in all cases, even for libraries that use proper equality checks.
why cant i "return expenses1" without spreading it in an array
Because the first rule of React state is do not modify state directly, which includes not modifying the state of objects that are held in state (like arrays), see the documentation. If you don't return a different array than the one in state, React doesn't see a difference, and doesn't necessarily re-render properly.
The code you have that is using spread is still technically incorrect (though largely harmless I suspect), it should be copying the array before adding to it (or at the same time), not adding it to the array in state (which mutates it, which you mustn't do). Creating a new array by copying a previous one and adding an element can be done with a single array literal:
setListOfExpenses((expenses) => {
return [...expenses, expenseData];
});
or even
setListOfExpenses((expenses) => [...expenses, expenseData]);

Add only unique items in a VUE.js v-for loop

I'm pulling a JSON formatted object from a Web API. The items are NIH funding grants and when pulled give a history of each Grant for a particular researcher. I'm only looking to render the max award_notice_date for each separate project_serial_num. I have the elements in descending order so if I do a v-for loop they are all rendered in descending order. So far so good. My question is how I can post only the first one of each of the project_serial_num's so I end up with the information from appl_id 10372234 and 10226173? Trying to filter by using dates does not help because a date may have expired but if it's the first one of that project_serial_num then it needs to be displayed
[{
"appl_id": 10372234,
"subproject_id": null,
"fiscal_year": 2022,
"project_num": "5R01CA261232-02",
"project_serial_num": "CA261232",
"award_notice_date": "2022-03-02T12:03:00Z"
},
{
"appl_id": 10226173,
"subproject_id": null,
"fiscal_year": 2021,
"project_num": "5K07CA214839-05",
"project_serial_num": "CA214839",
"award_notice_date": "2021-08-05T12:08:00Z"
},
{
"appl_id": 10253554,
"subproject_id": null,
"fiscal_year": 2021,
"project_num": "1R01CA261232-01",
"project_serial_num": "CA261232",
"award_notice_date": "2021-03-15T12:03:00Z"
},
{
"appl_id": 9989810,
"subproject_id": null,
"fiscal_year": 2020,
"project_num": "5K07CA214839-04",
"project_serial_num": "CA214839",
"award_notice_date": "2020-07-30T12:07:00Z"
}
]
I used this answer from another question to build this:
YOUR_ARRAY.filter((value, index, self) => self.findIndex(v => v.project_serial_num === value.project_serial_num) === index);
This will return a new array where the objects sharing a project_serial_num are removed, which can be passed directly to v-for, without the need to make a new variable/computed property.
You should create a computed property that will use a Map to store the values - but before adding a value to the Map you will first check whether it is already there and eventually skip the duplicates. Something like this
computed:
{
uniqueProjects()
{
const result = {};
this.apiResult.forEach(project =>
{
if (!result[project.project_serial_num])
{
result[project.project_serial_num] = project;
}
});
return Object.values(result); // we rely on the browser to return values in the order they have been added
}
}

Immutable JS - how to preserve Type and get immutability when converting a deeply nested JS object?

Working on a React, Redux + Typescript project, I am trying to add Immutable JS to the stack.
I started with working on a large nested object that could really use being safer as an immutable data structure.
import { Record, fromJS } from "immutable";
const obj = {
name: "werwr",
overview: {
seasons: {
2017: [{ period: 1, rates: 2 }]
}
}
};
// -- Using fromJS
const objJS = fromJS(obj);
const nObj = objJS.getIn(["overview", "seasons", "2017"]);
console.log(nObj); // I get an immutable list cool!
// -- Using Record, infer the type
const objRecord = Record(obj)();
const nRec = objRecord.getIn(["overview", "seasons", "2017"]);
console.log(nRec); // but I get a JS array
// -- Using both
const makeRec = Record(objJS);
const bothRecord = makeRec({ name: "name" });
console.log(bothRecord); // fails
Runnable code in codesandbox: https://codesandbox.io/s/naughty-panini-9bpgn?file=/src/index.ts
using fromJS. The conversion works well and deep but I lose all
type information.
using a Record. It keeps track of the type but nested arrays are
still mutable.
passing the converted object into a Record and manually add the type but I ran into an error: Cannot read property 'get' of
undefined
Whats the proper way to convert such an object to a fully immutable data structure while not loosing the type? Thanks!
You can use classes to construct deep structures.
interface IRole {
name: string;
related: IRole[];
}
const roleRecord = Record({
name: '',
related: List<Role>(),
});
class Role extends roleRecord {
name: string;
related: List<Role>;
constructor(config: IRole) {
super(Object.assign({}, config, {
related: config.related && List(config.related.map(r => new Role(r))),
}));
}
}
const myRole = new Role({
name: 'President',
related: [
{name: 'VP',
related:[
{name: 'AVP',
related: []}
]}
]});
With this type of structure, myRole will be all nested Role classes.
NOTE: I will add a bit of caution, we have been using this structure in a production application for almost 4 years now (angular, typescript, redux), and I added the immutablejs for safety from mutated actions and stores. If I had to do it over, the strict immutable store and actions that comes with NGRX would be my choice. Immutablejs is great at what it does, but the complexity it adds to the app is a trade off (Especially for onboarding new/greener coders).
Record is a factory for Record-Factories. As such, the argument should be an object template (aka default values), not actual data! (see docs).
const MyRecord = Record({
name: "werwr",
overview: null
});
const instance = MyRecord(somedata);
As you already noticed, the Record factory will not transform data to immutable. If you want to do that, you have to either do it manually with Maps and Lists, fromJS or the constructor of records.
The last approach is a bit weird, because then your record factory suddendly becomes a class:
const SeasonRecord = Record({
period: null, rates: null
})
class MyRecord extends Record({
name: "default_name",
seasons: Map()
}, 'MyRecord') {
constructor(values = {}, name) {
if(values.seasons) {
// straight forward Map of seasons:
// values = fromJS(values);
// Map of sub-records
values.seasons = Object.entries(values.seasons).reduce(
(acc, [year, season]) => {
acc[year] = SeasonRecord(season);
return acc;
}, {});
values.seasons = Map(values.seasons);
}
super(values, name);
}
}
const x = new MyRecord({
seasons: {
2017: { period: 1, rates: 2 }
}
})
console.log('period of 2017', x.seasons.get('2017').period)
I strongly suggest to not use unecessarily nest objects (record -> overview -> season) as it makes everything more complicated (and if you use large amounts of records, it might impact performance).
My general recommendation for Records is to keep them as flat as possible. The shown nesting of records allows to use the property access syntax instead of get, but is too tendious most of the time. Simply doing fromJS() for the values of a record and then use getIn is easier.

React Redux - Reducer with CRUD - best practices?

I wrote simple reducer for User entity, and now I want to apply best practices for it, when switching action types and returning state. Just to mention, I extracted actions types in separate file, actionsTypes.js.
Content of actionsTypes.js :
export const GET_USERS_SUCCESS = 'GET_USERS_SUCCESS';
export const GET_USER_SUCCESS = 'GET_USER_SUCCESS';
export const ADD_USER_SUCCESS = 'ADD_USER_SUCCESS';
export const EDIT_USER_SUCCESS = 'EDIT_USER_SUCCESS';
export const DELETE_USER_SUCCESS = 'DELETE_USER_SUCCESS';
First question, is it mandatory to have actions types for FAILED case? For example, to add GET_USERS_FAILED and so on and handle them inside usersReducer?
Root reducer is:
const rootReducer = combineReducers({
users
});
There is code of usersReducer, and I put comments/questions inside code, and ask for answers (what are best practices to handle action types):
export default function usersReducer(state = initialState.users, action) {
switch (action.type) {
case actionsTypes.GET_USERS_SUCCESS:
// state of usersReducer is 'users' array, so I just return action.payload where it is array of users. Will it automatically update users array on initial state?
return action.payload;
case actionsTypes.GET_USER_SUCCESS:
// What to return here? Just action.payload where it is just single user object?
return ;
case actionsTypes.ADD_USER_SUCCESS:
// what does this mean? Can someone explain this code? It returns new array, but what about spread operator, and object.assign?
return [...state.filter(user => user.id !== action.payload.id),
Object.assign({}, action.payload)];
case actionsTypes.EDIT_USER_SUCCESS:
// is this ok?
const indexOfUser = state.findIndex(user => user.id === action.payload.id);
let newState = [...state];
newState[indexOfUser] = action.payload;
return newState;
case actionsTypes.DELETE_USER_SUCCESS:
// I'm not sure about this delete part, is this ok or there is best practice to return state without deleted user?
return [...state.filter(user => user.id !== action.user.id)];
default:
return state;
}
}
I'm not an experienced developer but let me answer your questions what I've learned and encountered up to now.
First question, is it mandatory to have actions types for FAILED case?
For example, to add GET_USERS_FAILED and so on and handle them inside
usersReducer?
This is not mandatory but if you intend to give a feedback to your clients it would be good. For example, you initiated the GET_USERS process and it failed somehow. Nothing happens on client side, nothing updated etc. So, your client does not know it failed and wonders why nothing happened. But, if you have a failure case and you catch the error, you can inform your client that there was an error.
To do this, you can consume GET_USERS_FAILED action type in two pleases for example. One in your userReducers and one for, lets say, an error or feedback reducer. First one returns state since your process failed and you can't get the desired data, hence does not want to mutate the state anyhow. Second one updates your feedback reducer and can change a state, lets say error and you catch this state in your component and if error state is true you show a nice message to your client.
state of usersReducer is 'users' array, so I just return
action.payload where it is array of users. Will it automatically
update users array on initial state?
case actionsTypes.GET_USERS_SUCCESS:
return action.payload;
This is ok if you are fetching whole users with a single request. This means your action.payload which is an array becomes your state. But, if you don't want to fetch all the users with a single request, like pagination, this would be not enough. You need to concat your state with the fetched ones.
case actionsTypes.GET_USERS_SUCCESS:
return [...state, ...action.payload];
Here, we are using spread syntax.
It, obviously, spread what is given to it :) You can use it in a multiple ways for arrays and also objects. You can check the documentation. But here is some simple examples.
const arr = [ 1, 2, 3 ];
const newArr = [ ...arr, 4 ];
// newArr is now [ 1, 2, 3, 4 ]
We spread arr in a new array and add 4 to it.
const obj = { id: 1, name: "foo, age: 25 };
const newObj = { ...obj, age: 30 };
// newObj is now { id: 1, name: "foo", age: 30 }
Here, we spread our obj in a new object and changed its age property. In both examples, we never mutate our original data.
What to return here? Just action.payload where it is just single user
object?
case actionsTypes.GET_USER_SUCCESS:
return ;
Probably you can't use this action in this reducer directly. Because your state here holds your users as an array. What do you want to do the user you got somehow? Lets say you want to hold a "selected" user. Either you can create a separate reducer for that or change your state here, make it an object and hold a selectedUser property and update it with this. But if you change your state's shape, all the other reducer parts need to be changed since your state will be something like this:
{
users: [],
selectedUser,
}
Now, your state is not an array anymore, it is an object. All your code must be changed according to that.
what does this mean? Can someone explain this code? It returns new
array, but what about spread operator, and object.assign?
case actionsTypes.ADD_USER_SUCCESS:
return [...state.filter(user => user.id !== action.payload.id), Object.assign({}, action.payload)];
I've already tried to explain spread syntax. Object.assign copies some values to a target or updates it or merges two of them. What does this code do?
First it takes your state, filters it and returns the users not equal to your action.payload one, which is the user is being added. This returns an array, so it spreads it and merges it with the Object.assign part. In Object.assign part it takes an empty object and merges it with the user. An all those values creates a new array which is your new state. Let's say your state is like:
[
{ id: 1, name: "foo" },
{ id: 2, name: "bar" },
]
and your new user is:
{
id: 3, name: "baz"
}
Here what this code does. First it filters all the user and since filter criteria does not match it returns all your users (state) then spread it (don't forget, filter returns an array and we spread this array into another one):
[ { id: 1, name: "foo"}, { id: 2, name: "bar" } ]
Now the Object.assign part does its job and merges an empty object with action.payload, a user object. Now our final array will be like this:
[ { id: 1, name: "foo"}, { id: 2, name: "bar" }, { id: 3, name: "baz" } ]
But, actually Object.assign is not needed here. Why do we bother merging our object with an empty one again? So, this code does the same job:
case actionsTypes.ADD_USER_SUCCESS:
return [...state.filter(user => user.id !== action.payload.id), action.payload ];
is this ok?
case actionsTypes.EDIT_USER_SUCCESS:
const indexOfUser = state.findIndex(user => user.id === action.payload.id);
let newState = [...state];
newState[indexOfUser] = action.payload;
return newState;
It seems ok to me. You don't mutate the state directly, use spread syntax to create a new one, update the related part and finally set your state with this new one.
I'm not sure about this delete part, is this ok or there is best
practice to return state without deleted user?
case actionsTypes.DELETE_USER_SUCCESS:
return [...state.filter(user => user.id !== action.user.id)];
Again, it seems ok to me. You filter the deleted user and update your state according to that. Of course there are other situations you should take into considiration . For example do you have a backend process for those? Do you add or delete users to a database? If yes for all the parts you need to sure about the backend process success and after that you need to update your state. But this is a different topic I guess.

How to update key value in immutable while filtering over List of Maps

I have an immutable List that looks like this:
this.state = {
suggestedUsers: fromJS([
{
user: {
client_user_id: "1234567890",
full_name: "marty mcfly",
image: "imageURL",
role_name: "Associate Graphic Designer",
selected: false
}
},
{
user: {
client_user_id: "0987654321",
full_name: "doc",
image: "imageURL",
role_name: "Software Engineer",
selected: false
}
}
)]
This is used in a div that displays this information in the UI.
When I click on the div, I have a function that is fired that looks like this:
selectUser(clientUserId){
// set assessments variable equal to the current team from the state
let assessments = fromJS(this.state.suggestedUsers)
let selectAssessor
// set a variable called selectedUsers equal to the results of filtering over the current suggestedUsers from the state
let selectedUsers = assessments.filter((obj) => {
// store immutable retrieval of the client user id in a variable called userId
let userId = obj.getIn(["user", "client_user_id"])
// when the user clicks 'Add' user, if the id of the user matches the selected user id
// the user, represented here by obj, is pushed into the selectedUsers array stored in the state.
if(userId === clientUserId){
return obj.setIn(["user", "selected"], true)
}
// If the user id is not equal to the selected user, that team member is kept in the
// current team array represented by the state.
return userId !== clientUserId
})
// update the state with the current representation of the state determined by the user
// selected team members for assessment requests
this.setState({
suggestedUsers: selectedUsers
})
}
The core of my question is this:
I would like to update the value of the 'selected' key in the users object to false, when this function is invoked.
I'm aware that I can't mutate the List I'm filtering over directly, but I've tried may different approaches to getting the selected value updated (i.e. using updateIn, and setIn). I know I need to set the result of calling setIn to a variable, and return that to the List I'm filtering over, but I can't get the value to update in the existing List. Any help is greatly appreciated. Thanks!
I've verified that this works the way it should when I change the value manually. How can I change it with immutable by updating this one List.
=========================================================================
Thank you to the community for your feedback. Filtering, and mapping did turn out to be overkill. Using immutability-helper, I am able to update the selected value of a particular user at the index that is clicked. One caveat that was not mentioned is using merge to bring your updated data into your previous data. After updating with immutability helper, I push the updated value into an array, then make it a List, and merge it into my original data. Code below:
let users = this.state.teamAssessments
let selectedArray = []
users.map((obj, index) => {
let objId = obj.getIn(["user", "client_user_id"])
if(objId === clientUserId){
const selectedUser = update(this.state.teamAssessments.toJS(), {
[index]: {
user : {
selected: {
$set: true
}
}
}
})
selectedArray.push(selectedUser)
}
})
let updatedArray = fromJS(selectedArray).get(0)
let mergedData = users.merge(updatedArray)
this.setState({
teamAssessments: mergedData
})
You need immutability-helper. Basically, instead of cloning the entire object you just modify small pieces of the object and re-set the state after you are finished.
import update from 'immutability-helper';
const newData = update(myData, {
x: {y: {z: {$set: 7}}},
a: {b: {$push: [9]}}
});
this.setState({varName: newData});
In other words, I would ditch the fromJS and the modifying of the array while enumerating it. First, enumerate the array and create your updates. Then, apply the updates separately. Also, to me the "selected" var seems redundant as you know if they are selected because the name of the array after filtration is "selectedUsers."
If I understand your question correctly, here's what I would suggest:
selectUser(clientUserId) {
let suggestedUsers = this.state.suggestedUsers.map(
userBlock =>
userBlock.setIn(
['user', 'selected'],
userBlock.getIn(['user', 'client_user_id']) === clientUserId
)
);
this.setState({
suggestedUsers,
});
}
To confirm -- you are just trying to modify state.suggestedUsers to have selected be true for the selected user, and false for everyone else? Sounds perfect for Immutable's map function (rather than filter, which will just return the elements of the list for which your predicate function returns something truthy).
BTW, you have an error in your object passed into fromJS -- you need an extra }, after the first assessor_assessment block.

Categories

Resources