How to make state immutable in React - javascript

I am looking for the best way to make my handleSwitch + setState function immutable. The code I have currently works but is mutable and I am unsure how to make it immutable everything I have tried either doesn't work or gives me a syntax error.
This is my state being initialised
this.state = {
contacts: [],
filteredContacts: [],
selected: [],
contactsSelection: {},
loading: false,
};
I have a simple <Switch /> comp which triggers the below when a user toggles the switch.
handleSwitch = async (contact, added) => {
if (added) {
console.log('added', { contact, added });
return this.setState(prevState => ({
contactsSelection: {
...prevState.contactsSelection,
[contact.id]: contact,
},
}));
}
console.log('removed', { contact, added });
// const { contactsSelection } = this.state;
return this.setState(prevState => {
const existingSelection = { ...prevState.contactsSelection };
delete existingSelection[contact.id]; // <-- make immutable
return { contactsSelection: existingSelection };
});
};

just to clarify: your code is fine by now, it does not mutate state directly but mutates copy.
but if you'd like having that in different way, here is a pattern:
this.setState(prevState => {
const { [contact.id]: unusedVariable, ...restContacts } =
prevState.contactsSelection;
return { contactsSelection: restContacts };
})
or with destructuring:
this.setState(
({ contactsSelection: {[contact.id]: unused, ...filteredContact} })) =>
({ contactsSelection: filteredContacts })
);
Anyway unfortunately there is no way to avoid introducing unusedVariable while destructuring. So it will need you to suppress ESLint warning "variable ... is not used".
To me, your way handling that is better readable.

Related

React state variable updates automatically without calling setState

I am facing the following issue and not able to figure it out.
I have two variables inside the state called userDetails & userDetailsCopy. In componentDidMount I am making an API call and saving the data in both userDetails & userDetailsCopy.
I am maintaining another copy called userDetailsCopy for comparison purposes.
I am updating only userDetails inside setState but even userDetailsCopy is also getting updated instead of have old API data.
Below is the code :
constructor(){
super()
this.state={
userDetails:{},
userDetailsCopy: {}
}
}
componentDidMount(){
// API will return the following data
apiUserDetails : [
{
'name':'Tom',
'age' : '28'
},
{
'name':'Jerry',
'age' : '20'
}
]
resp.data is nothing but apiUserDetails
/////
apiCall()
.then((reps) => {
this.setState({
userDetails: resp.data,
userDetailsCopy: resp.data
})
})
}
updateValue = (text,i) => {
let userDetail = this.state.userDetails
userDetail[i].name = text
this.setState({
userDetails: userDetail
})
}
submit = () => {
console.log(this.state.userDetials) // returns updated values
console.log(this.state.userDetailsCopy) // also return updated values instead of returning old API data
}
Need a quick solution on this.
The problem with this is that you think you are making a copy of the object in state by doing this
let userDetail = this.state.userDetails
userDetail.name = text
But, in Javascript, objects are not copied like this, they are passed by referrence. So userDetail at that point contains the referrence to the userDetails in your state, and when you mutate the userDetail it goes and mutates the one in the state.
ref: https://we-are.bookmyshow.com/understanding-deep-and-shallow-copy-in-javascript-13438bad941c
To properly clone the object from the state to your local variable, you need to instead do this:
let userDetail = {...this.state.userDetails}
OR
let userDetail = Object.assign({}, this.state.userDetails)
Always remember, Objects are passed by referrence not value.
EDIT: I didn't read the question properly, but the above answer is still valid. The reason userDetailCopy is being updated too is because resp.data is passed by referrence to both of them, and editing any one of them will edit the other.
React state and its data should be treated as immutable.
From the React documentation:
Never mutate this.state directly, as calling setState() afterwards may
replace the mutation you made. Treat this.state as if it were
immutable.
Here are five ways how to treat state as immutable:
Approach #1: Object.assign and Array.concat
updateValue = (text, index) => {
const { userDetails } = this.state;
const userDetail = Object.assign({}, userDetails[index]);
userDetail.name = text;
const newUserDetails = []
.concat(userDetails.slice(0, index))
.concat(userDetail)
.concat(userDetails.slice(index + 1));
this.setState({
userDetails: newUserDetails
});
}
Approach #2: Object and Array Spread
updateValue = (text, index) => {
const { userDetails } = this.state;
const userDetail = { ...userDetails[index], name: text };
this.setState({
userDetails: [
...userDetails.slice(0, index),
userDetail,
...userDetails.slice(index + 1)
]
});
}
Approach #3: Immutability Helper
import update from 'immutability-helper';
updateValue = (text, index) => {
const userDetails = update(this.state.userDetails, {
[index]: {
$merge: {
name: text
}
}
});
this.setState({ userDetails });
};
Approach #4: Immutable.js
import { Map, List } from 'immutable';
updateValue = (text, index) => {
const userDetails = this.state.userDetails.setIn([index, 'name'], text);
this.setState({ userDetails });
};
Approach #5: Immer
import produce from "immer";
updateValue = (text, index) => {
this.setState(
produce(draft => {
draft.userDetails[index].name = text;
})
);
};
Note:
Option #1 and #2 only do a shallow clone. So if your object contains nested objects, those nested objects will be copied by reference instead of by value. So if you change the nested object, you’ll mutate the original object.
To maintain the userDetailsCopy unchanged you need to maintain the immutability of state (and state.userDetails of course).
function getUserDerails() {
return new Promise(resolve => setTimeout(
() => resolve([
{ id: 1, name: 'Tom', age : 40 },
{ id: 2, name: 'Jerry', age : 35 }
]),
300
));
}
class App extends React.Component {
state = {
userDetails: [],
userDetailsCopy: []
};
componentDidMount() {
getUserDerails().then(users => this.setState({
userDetails: users,
userDetailsCopy: users
}));
}
createChangeHandler = userDetailId => ({ target: { value } }) => {
const { userDetails } = this.state;
const index = userDetails.findIndex(({ id }) => id === userDetailId);
const userDetail = { ...userDetails[index], name: value };
this.setState({
userDetails: [
...userDetails.slice(0, index),
userDetail,
...userDetails.slice(index + 1)
]
});
};
render() {
const { userDetails, userDetailsCopy } = this.state;
return (
<React.Fragment>
{userDetails.map(userDetail => (
<input
key={userDetail.id}
onChange={this.createChangeHandler(userDetail.id)}
value={userDetail.name}
/>
))}
<pre>userDetails: {JSON.stringify(userDetails)}</pre>
<pre>userDetailsCopy: {JSON.stringify(userDetailsCopy)}</pre>
</React.Fragment>
);
}
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Access the state of my redux app using redux hooks

I am migrating my component from a class component to a functional component using hooks. I need to access the states with useSelector by triggering an action when the state mounts. Below is what I have thus far. What am I doing wrong? Also when I log users to the console I get the whole initial state ie { isUpdated: false, users: {}}; instead of just users
reducers.js
const initialState = {
isUpdated: false,
users: {},
};
const generateUsersObject = array => array.reduce((obj, item) => {
const { id } = item;
obj[id] = item;
return obj;
}, {});
export default (state = { ...initialState }, action) => {
switch (action.type) {
case UPDATE_USERS_LIST: {
return {
...state,
users: generateUsersObject(dataSource),
};
}
//...
default:
return state;
}
};
action.js
export const updateUsersList = () => ({
type: UPDATE_USERS_LIST,
});
the component hooks I am using
const users = useSelector(state => state.users);
const isUpdated = useSelector(state => state.isUpdated);
const dispatch = useDispatch();
useEffect(() => {
const { updateUsersList } = actions;
dispatch(updateUsersList());
}, []);
first, it will be easier to help if the index/store etc will be copied as well. (did u used thunk?)
second, your action miss "dispatch" magic word -
export const updateUsersList = () =>
return (dispatch, getState) => dispatch({
type: UPDATE_USERS_LIST
});
it is highly suggested to wrap this code with { try } syntax and be able to catch an error if happened
third, and it might help with the console.log(users) error -
there is no need in { ... } at the reducer,
state = intialState
should be enough. this line it is just for the first run of the store.
and I don't understand where { dataSource } comes from.

React Redux Connect MapState not updating for a filtered item from a collection

I think I've either misunderstood something, or am doing something deeply wrong, when attempting to subscribe to changes on a specific item in a collection in my store. Unless I add a direct list subscription, my component does not receive updates.
The following works:
const mapStateToProps = (state, props) => ({
list: state.podcasts.items,
getItem: props.id
? state.podcasts.items.filter(item => item.clientId === props.id)[0] || {}
: {},
});
If I remove the list item I only receive the the initial state of the collection item I'm subscribing to.
How I'm updating the list in the reducer:
PODCAST_GRADIENT_UPDATED: (state, { payload }) => ({
...state,
items: state.items.map(item => {
if (item.clientId === payload.clientId) {
item.gradient = payload.gradient; // eslint-disable-line
}
return item;
}),
}),
Should the above work without the list subscription?
if not, how should this be done?
this was a rookie error, in that there is some state mutation in the above example. changing my function in the reducer in the following way resolved this:
PODCAST_GRADIENT_UPDATED: (state, { payload }) => ({
...state,
items: state.items.map(item => {
if (item.clientId === payload.clientId) {
return { ...item, gradient: payload.gradient };
}
return { ...item };
}),
}),
notice specifically the use of the spread operator to create and return a new item.

Basic Filtering in react-data-grid

I am recently exploring "react-data-grid" a bit more and trying to get the Basic Filtering feature working in my React app. This is their Basic Filtering example I am looking at:
http://adazzle.github.io/react-data-grid/docs/examples/column-filtering
Their example code in codesandbox:
https://codesandbox.io/s/w6jvml4v45?from-embed
I copied most of the code artifacts examples into my class component and tried to find a equivalent solution for React's useState() (I am quite new to React and useState apparently not available in classes).
My code has been modified slightly for this forum and is pointing to the public JSONPlaceholder website to simulate a REST API call from a real server with test data. So hopefully you can just run it. Here is my App.js code:
import React, { Component } from "react";
import ReactDataGrid from "react-data-grid";
import { Toolbar, Data } from "react-data-grid-addons";
import "./App.css";
import "bootstrap/dist/css/bootstrap.css";
const defaultColumnProperties = {
filterable: true,
width: 120,
editable: true
};
const columns = [
{ key: "id", name: "ID" },
{ key: "username", name: "Username" },
{ key: "email", name: "Email" }
].map(c => ({ ...c, ...defaultColumnProperties }));
function getRows(rows, filters) {
const selectors = Data.Selectors;
return selectors.getRows({ rows, filters });
}
class App extends Component {
constructor(props) {
super(props);
this.state = {
rows: [],
isLoaded: false,
filters: {},
setFilters: {}
};
}
componentDidMount() {
fetch("https://jsonplaceholder.typicode.com/users")
.then(res => res.json())
.then(json => {
this.setState({
isLoaded: true,
rows: json
});
});
}
onGridRowsUpdated = ({ fromRow, toRow, updated }) => {
this.setState(state => {
const rows = state.rows.slice();
for (let i = fromRow; i <= toRow; i++) {
rows[i] = { ...rows[i], ...updated };
}
return { rows };
});
};
render() {
// const [filters, setFilters] = useState({});
const filteredRows = getRows(this.state.rows, this.state.filters);
// Commenting ORIGINAL handleFilterChange example code temporarily
// const handleFilterChange = filter => filters => {
// const newFilters = { ...filters };
// if (filter.filterTerm) {
// newFilters[filter.column.key] = filter;
// } else {
// delete newFilters[filter.column.key];
// }
// return newFilters;
// };
// Temporarily rewrote handleFilterChange function for DEBUGGING purpose and not using arrow fucntions
// Used babeljs.io to generate non arrow based handleFilterChange function.
var handleFilterChange = function handleFilterChange(filter) {
debugger;
console.log("handleFilterChange(filter)" + filter);
return function(filters) {
debugger;
console.log("function(filters)" + filters);
var newFilters = { ...filters };
if (filter.filterTerm) {
newFilters[filter.column.key] = filter;
} else {
delete newFilters[filter.column.key];
}
return newFilters;
};
};
return (
<ReactDataGrid
columns={columns}
rowGetter={i => filteredRows[i]}
rowsCount={filteredRows.length}
minHeight={500}
toolbar={<Toolbar enableFilter={true} />}
onAddFilter={filter =>
this.setState({ setFilters: handleFilterChange(filter) })
}
onClearFilters={() => this.setState({ setFilter: {} })}
/>
);
}
}
export default App;
As per comments in the code example above I used babeljs.io to generate a non arrow based handleFilterChange function and added some logs and debugging statements
For some reason the nested function:
return function(filters) {
var newFilters = {
...filters
};
if (filter.filterTerm) {
newFilters[filter.column.key] = filter;
} else {
delete newFilters[filter.column.key];
}
doesn't get called. The debugger in Chrome doesn't hit the break point or prints out any debugging logs I added.
This log gets always called:
console.log("handleFilterChange(filter)" + filter);
This log in the inner function never gets called which I believe is the problem?
console.log("function(filters)" + filters);
The non arrow function based handleFilterChange works when I use it in their example and replaced their handleFilterChange code so I believe the code itself is fine and all debug logs appear in the console. The inner function gets called as well. Happy to use the arrow function though if I can get a bit help to get this working.
I've also wrote a not class based Basic Filter version but run into problems when loading the quite huge JSON data from the server. Didn't investigate that further but I believe it was a timing issue.
Due to the problem, the table gets loaded in the browser and I can press on the "Filter Rows" button in the top right corner. This will fold down the search edit boxes and I can type in letters but the table doesn't get filtered on the fly while typing letters.
handleFilterChange(filter) {
let newFilters = Object.assign({}, this.props.filters);
if (filter.filterTerm) {
newFilters[filter.column.key] = filter;
} else {
delete newFilters[filter.column.key];
}
this.setState({ filters: newFilters });
}

Best way to update / change state object in react native?

What's the best way to update a nested property deep inside the State object?
// constructor --
this.state.someprop = [{quadrangle: {rectangle: {width: * }}, ...}]
...
I want to update width of the rectangle object.
this.state.quadrangle.rectangle.width = newvalue // isn't working
I could make it work like:
const {quadrangle} = this.state
quadrangle.rectangle.width = newvalue
this.setState = {
quadrangle: quadrangle
}
But this method doesn't sound the best way for performance/memory
// ES6 WAYS TO UPDATE STATE
// NOTE: you MUST use this.setState() function for updates to your state
class Example extends Component {
constructor(props) {
super(props);
this.state = {
name: 'John',
details: {
age: 28,
height: 1.79,
}
}
componentDidMount() {
this.handleChangeName('Snow');
this.handleAgeChange(30);
}
componentDidUpdate() {
console.log(this.state);
/*
returns
{
name: 'Snow',
details: {
age: 30,
height: 1.79,
}
}
*/
}
// this way you keep your previous state immutable (best practice) with
// param "prevState"
handleChangeName = (_name) => {
this.setState(
(prevState) => ({
name: _name
})
)
}
//this is how you update just one property from an internal object
handleAgeChange = (_age) => {
this.setState(
(prevState) => ({
details: Object.assign({}, prevState.details, {
age: _age
})
})
)
}
// this is the simplest way to set state
handleSimpleAgeChange = (_age) => {
this.setState({
details: Object.assign({}, this.state.details, { age: _age })
})
}
render() {
return (
<h1>My name is {this.state.name} and I'm {this.state.details.age} years old</h1>
)
}
}
If you want to keep the best practice without making it harder, you can do:
updateState = (obj) => {
if (obj instance of Object) {
this.setState(
(prevState) => (Object.assign({}, prevState, obj))
);
}
}
usage:
//code ... code ... code ...
handleAgeChange = (_age) => {
this.updateState({
details: Object.assign({}, this.state.details, { age: _age }
})
}
The best way, and the way facebook has proposed, is to use this.setState({someProp: "your new prop"}).
Using it is the only way which is going to guarantee that the component will be rendered correctly.
This function is incremental, so you dont need to set the whole state, just the prop you need.
I strongly recomend you to read the docs here.
If your object is nested make the inner object it's own state,
this.state = {
quadrangle: {this.state.rectangle, ...}
rectangle: {width: * }}
};
then use your clone and replace technique:
const {rectangleNew} = this.state.rectangle;
rectangleNew.width = newvalue;
this.setState({rectangle: rectangleNew});
The state should propagate upwards. Should improve performance if only certain quadrangles need to be updated based on said rectangle. Props down, state up.
with hooks use this way
- setBorder((pre) => { return ({ ...pre, border_3: 2 }) })
example :
// state for image selected [ borderd ]
const [bordered, setBorder] = useState({ border_1: 0, border_2: 0, border_3: 0, border_4: 0, border_5: 0, border_6: 0, border_7: 0, border_8: 0 });
// pre is previous state value
const handle_chose = (y) => {
//generate Dynamic Key
var key = "border_" + y;
setBorder((pre) => { return ({ ...pre, [key]: 2 }) })
}

Categories

Resources