React state comparison with objects and nested arrays - reactjs

I am trying to compare state against a previous version of the same object (useRef) to detect for changes.
The object that loads into state with useEffect looks like this:
{
active: true,
design: [
{
existing: '55Xgm53McFg1Bzr3qZha',
id: '38fca',
options: ['John Smith', 'Jane Doe'],
required: true,
title: 'Select your name',
type: 'Selection List'
},
{
existing: '55Xgm53McFg1Bzr3qZha',
id: '38fca',
options: ['John Smith', 'Jane Doe'],
required: true,
title: 'Select your name',
type: 'Selection List'
},
],
icon: '🚜',
id: '92yIIZxdFYoWk3FMM0vI',
projects: false,
status: true,
title: 'Prestarts'
}
This is how I load in my app object and define a comparitor state (previousApp) to compare it and detect changes (with lodash) this all works great until I change a value in the design array.
const [app, setApp] = useState(null)
const [edited, setEdited] = useState(false)
const previousApp = useRef(null)
useEffect(() => {
if (app === null) {
getApp(someAppId).then(setApp).catch(window.alert)
}
if (previousApp.current === null && app) {
previousApp.current = { ...app }
}
if (previousApp.current && app) {
_.isEqual(app, previousApp.current) ? setEdited(false) : setEdited(true)
}
}, [app])
For example I change the input value of app.design[0].title = 'testing' with the following code:
const updateItem = e => {
let newApp = { ...app }
let { name, value } = e.target
newApp.design[0][name] = value
setApp(newApp)
}
This works in the sense that it updates my app state object but not that it detects any changes in comparison to previousApp.current
When I console log app and previousApp after changing both values, they are identical. Its seems to update my previousApp value aswell for some reason.
Rearranging the design array works as expected and detects a change with the following function:
const updateDesign = e => {
let newApp = { ...app }
newApp.design = e
setApp(newApp)
}

It probably wasn't detecting any difference because the array present in the design property was keeping the same reference.
When you do this:
let newApp = { ...app }
You're creating a new object for your app, but the property values of that object will remain the same, because it's a shallow copy. Thus, the design array (which is an object, and therefore is handled by reference) will keep its value (the reference) unaltered.
I think this would also solve your problem:
const updateItem = e => {
let newApp = { ...app };
let newDesign = Array.from(app.design); // THIS CREATES A NEW ARRAY REFERENCE FOR THE 'design' PROPERTY
let { name, value } = e.target;
newDesign[0][name] = value;
setApp({
...newApp,
design: newDesign
});
}

I found that using lodash's cloneDeep function i was able to remove the strange link that was occuring between previousApp and App; Even though I used {...app}
See the following:
if (previousApp.current === null && app) {
previousApp.current = cloneDeep(app)
}

Related

How to update specific object property in React state

I'm trying to make my code more concise and therefore removed all of these from separate useState hooks and put them into one. But I'm now trying to update the visible property of the objects based on a toggle state. I've attempted I've looked through different answers about immutable state / mapping through object etc. But it's confusing me even more. What do I put in between my function to change state on toggle change?
const [columnsVisible, setColumnsVisible] = useState({
column1: { visible: true },
column2: { visible: true },
column3: { visible: true },
column4: { visible: true },
});
const changeColumnVisibility = () => {
}
return(
{columnsVisible.column1.visible &&
(
<Column
<>
{toOrderState.map((job, index) => (
<JobCard
job_number={job.jobNumber}
time={job.time}
/>
))}
</>
/>
)}
)
One option to achieve immutability is by using Spread operator. As an example:
const myObject = { a: 1, b: 'some text', c: true };
const myCopy = { ...myObject };
results in myCopy being identical to myObject but changing myCopy (e.g. myCopy.a = 2) will not change myObject.
Now extend that so that myObject has more complex stuff in it instead of just an integer or a boolean.
const myObject = { a: { visible: true, sorted: 'ASC' } };
const myCopy = { ...myObject };
The thing here is that if you change myCopy's visible (for example, myCopy.a.visible = false) you will be changing myObject (myObject.a.visible will also be false even though you haven't explicitly changed myObject).
The reason for the above is that the Spread operator is shallow; so when you are making a copy of a you really are making a copy of the reference to the original a ({ visible: true, sorted: 'DESC' }). But guess what, you can use the Spread operator for copying a.
const myObject = { a: { visible: true, sorted: 'ASC' } };
const myCopy = { a: { ...myObject.a } };
Before addressing specific question, there is one more interesting thing about the Spread operator... you can overwrite some of the copied values simply by adding a key to the new object
const myObject = { a: 1, b: 'some text', c: true };
const myCopy = { ...myObject, a: 2 };
with the above, myCopy.a will be 2 rather than `1.
So now think about what needs to change when changeColumnVisibility is called... you want to change one of the column details but not the others
const [columnsVisible, setColumnsVisible] = useState({
column1: { visible: true },
column2: { visible: true },
column3: { visible: true },
column4: { visible: true },
});
// columnName is either `'column1'`, `'column2'`, ...
const changeColumnVisibility = (columnName) => {
setColumnsVisible((currentColumnsVisible) => {
const isVisible = currentColumnsVisible[columnName].visible;
return {
...currentColumnsVisible,
[columnName]: { ...currentColumnsVisible[columnName], visible: !isVisible }
};
});
}
[columnName] is important to be used so that you actually end up setting column1 or column2 or whatever rather than adding an attribute called columnName.
The final thing to call out from the snippet is that I'm using setColumnsVisible with a callback to ensure that the values are changed correctly rather than using columnsVisible directly.
Hope this helps

React re-render problem with same data but different object of API

I am hitting a backend API which contains array of objects. I am storing the API data in a state called reportsTabData. This state contains array of objects and one object who's key is reports contains array of objects called options and each options contains array of objects called fields. I am setting the fields array of objects in my state called optionsTabData and I am creating a UI based on optionsTabData.
Example:
[
..some objects,
{
key: "report",
default: "incoming"
options: [
{
type: "incoming",
fields: [
{
colspan: "1"
key: "CLAppSchedulerRepDependentSelectors"
label: "Extension(s) Selection"
type: "CLAppSchedulerRepDependentSelectors"
}
]
},
{
type: "outgoing",
fields: [
{
colspan: "1"
key: "CLAppSchedulerRepDependentSelectors"
label: "Extension(s) Selection"
type: "CLAppSchedulerRepDependentSelectors"
}
]
}
]
}
]
The code works fine and UI is generated, but as you see, there are some reports who's fields contain same objects. The problem is the state update is not re-rendering for such use-case else it works fine.
Below is the code:
const [loader, setLoader] = useState(false);
const [optionsTabData, setOptionsTabData] = useState([]);
useEffect(() => {
callForApi()
},[])
const callForApi = async () => {
setLoader(true);
let response = await getRequest("api-url");
if (response.status == 200 && response.status <= 300) {
let adapter = response.data.schema;
setAdapterData(adapter);
setReportsTabData(adapter.report.fields);
setDistributionTabData(adapter.distribution.fields);
let data = [];
for (const ele of adapter.report.fields) {
if (ele.key === "report") {
for (const item of ele.options) {
if (item.type === ele.default) {
data = [...item.fields];
break;
}
}
break;
}
}
setOptionsTabData(data);
} else {
toastErrorMessagePopUp(response.data.detail);
}
setLoader(false);
};
const handleDropdownChange = (key, value) => {
if (key === "report") {
const schema = [...reportsTabData];
let report = schema.find((ele) => ele.key === "report");
let newOptions = []
for (const ele of report.options) {
if (ele.type === value) {
newOptions=[...ele.fields];
break;
}
}
setOptionsTabData([...newOptions])
}
}
How do I solve this? Apparently, doing something like await setOptionsTabData([]) before setOptionsTabData([...newOptions]) solves it but I think using await is a bad idea and also the value reflecting after a click in dropdown is sluggish and slow.
You can "force" the re-render by using a combination of JSON.stringify and JSON.parse
setOptionsTabData(JSON.parse(JSON.stringify(newOptions)))
It looks like it should be working. You are passing new array to setOptionsTabData which should trigger a rerender.
Using await is not a solution - functions returned by useState are synchronous and that should not make a difference. What's actually happening is you are resetting your optionsData state to an empty array and then setting it to the desired data.
Is there more code you can show? Maybe you have a useEffect somewhere that updates your UI with unfulfilled dependencies?

In React object updates but does not pass updated object to next function

I am using React and have the following object:
const [records, setRecords] = useState(
[
{id: 1, correct: false},
{id: 2, correct: false},
{id: 3, correct: false},
{id: 4, correct: false},
{id: 5, correct: false}
]);
To update the object I have the following:
const onCorrectAnswerHandler = (id, correct) => {
setRecords(
records.map((record) => {
if (record.id === id) {
return { id: id, correct: true };
} else {
return record;
}
})
);
}
Here's the problem:
I want to run another function called isComplete after it but within the Handler function, that uses the changed records object, but it appears to use the original unchanged 'records' object (which console.log confirms).
e.g.
const onCorrectAnswerHandler = (id, correct) => {
setRecords(
records.map((record) => {
if (record.id === id) {
return { id: id, correct: true };
} else {
return record;
}
})
);
isComplete(records);
}
Console.log(records) confirms this. Why does it not use the updated records since the isComplete function runs after the update, and how can I get it to do this?
Try renaming the function as React sees no change in the object and likewise when you are using an array or object in a state. Try to copy them out by storing them in a new variable.
setRecords(
const newRecords = records.map((record) => {
if (record.id === id) {
return { id: id, correct: true };
} else {
return record;
}
})
//seting this now triggers an update
setRecords(newRecords);
);
Then as per react documentation it's better to listen to changes with lifecycle methods and not setting state immediately after they are changed because useState is asynchronous.
so use useEffect to listen to the changes to set is Complete
useEffect(() => {
isComplete(records)
}, [records])
I hope this helps you?
This is due to the fact that setState is not actually synchronous. The stateful value is not updated immediately when setState is called, but on the next render cycle. This is because React does some behind the scenes stuff to optimise re-renders.
There are multiple approaches to get around this, one such approach is this:
If you need to listen to state updates to run some logic you can use the useEffect hook.
useEffect(() => {
isComplete(records)
}, [records])
This hook is pretty straightforward. The first argument is a function. This function will run each time if one of the variables in the dependency array updates. In this case it will run each time records update.
You can modify above function onCorrectAnswerHandler to hold the updated records in temporary variable and use to update state and call isComplete func
const onCorrectAnswerHandler = (id, correct) => {
let _records = records.map((record) => {
if (record.id === id) {
return {
id: id,
correct: true
};
} else {
return record;
}
})
setRecords(_records);
isComplete(_records);
}
try this please.
const onCorrectAnswerHandler = (id) => {
records.forEach(r =>{
if(r.id == id) r.correct=true;
});
setRecords([...records]);
}

Functional component problems React

I transformed a class component into a functional component but it looks like it does not work in a way it suppose to work and I can not find what is wrong. When I create a new object there is no name for the object and when I try to mark the object as a complete it removes all created objects at ones. I created a codesandbox here. Unfortunately, I am not too much familiar with functional component. Any help would be appreciated.
Here is my codesandbox sample:
https://codesandbox.io/s/crazy-sid-09myu?file=/src/App.js
Your Todos:
const [todos, setTodos] = useState([
{ id: uuid(), name: "Task 1", complete: true },
{ id: uuid(), name: "Task 2", complete: false }
]);
onAddHandler:
const addTodo = () =>
setTodos([...todos, { id: uuid(), name: "New Task", complete: false }]);
onSetCompleteHandler:
const setCompleteHandler = id =>
setTodos(
todos.map(todo => {
if (todo.id === id) {
return {
...todo,
complete: todo.complete ? 0 : 1
};
}
return todo;
})
);
I have created your new todos. Check out this link
Todos App
I have updated your code, please check the URL https://codesandbox.io/s/determined-morning-n8lgx?file=/src/App.js
const onComp = id => {
for (let i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
let t = { ...todos[i] };
t.complete = !t.complete;
todos[i] = t;
}
}
setTodos([...todos]); // Here todos reference need to be changed
};
And also
const onSubmit = event => {
event.preventDefault();
setTodos([
...todos,
{
id: generateNewId(),
name: newTodoName,
complete: false
}
]);
setNewTodoName("");
};
While using hooks we need to be careful about state variable updates. While manipulating arrays and objects use the spread operator to create new references which invokes child components and re-render current component.

Hooks and immutable state

I am apologising in advance if this questions has already been answered before (and also for the long post, but I've tried to be as specific as I could be). But, the answers I found does not completely satisfy me.
I want to use the new amazing React Hooks for my project. And for what I've been doing so far, it has been straight forward.
Now I have ran into a more complex example, and I feel unsure on how I best should tackle this one.
Let's say, I have more of a complex object (at least it's not flat) in my state.
{
persons: [
{
id: 1,
name: 'Joe Doe',
age: 35,
country: 'Spain',
interests: [
{ id: 1, en: 'Football', es: 'Fútbol' },
{ id: 2, en: 'Travelling', es: 'Viajar' }
]
},
{
id: 2,
name: 'Foo Bar',
age: 28,
country: 'Spain',
interests: [
{ id: 3, en: 'Computers', es: 'Computadoras' },
{ id: 4, en: 'Cooking', es: 'Cocinar' }
]
}
],
amount: 2,
foo: 'bar'
}
What is the best way to:
Add an item (an object) to my colleagues array
Add an item to a specific "interests" array?
Manipulate the value of a property within an object in the array?
Change a value outside the persons array, for example foo?
using the useState hook?
The following code examples will try to illustrate each question. They're not tested...
Let us consider I have a container that begins with this.
It also includes the functions that are split up in the rest of the post.
const [colleagues, setColleagues] = useState({});
// using the useEffect as "componentDidMount
useEffect(() => {
// receiving data from ajax request
// and set the state as the example object provided earlier
setColleagues(response.data);
}, []);
1) So for the first question. Is this valid?
Or do I need to to make sure that each and every object in my persons array is destructured?
const onAddNewColleague = () => {
// new colleague, this data would be dynamic
// but for the sake of this example I'll just hard code it
const newColleague = {
name: 'Foo Baz Bar',
age: 30,
country: 'Spain',
interests: [
{ en: 'Cats', es: 'Gatos' }
]
};
// creates a copy of the state
// targets the "persons" prop and adds a new array
// where the new colleague is appended
const newState = {
...colleagues,
persons: colleagues.concat(newColleague)
};
// updates the state
setColleagues(newState);
};
2) This feels wrong as I end up updating the entire persons array instead of just the interest array for a specific person.
const onAddNewInterest = (personId) => {
// a new interest
const newInterest = {
en: 'Languages', es: 'Idiomas'
};
// find the person according to personId
// and update the interests
const updatedPersons = colleagues.persons.map(person => {
if(person.id === personId) {
return {
...person,
interests: person.interests.concat(newInterest);
};
}
return person;
});
// create a copy of the state
const newState = {
...colleagues,
persons: [...updatedPersons]
};
setColleagues(newState);
};
3) As the second example, this one feels wrong too as I am updated the entire persons array when in fact I might just want to change the age of one specific person
const onChangeValue = (personId, key, value) => {
// find the person
const updatedPersons = colleagues.persons.map(person => {
if(person.id === personId) {
// key, could be age?
return {
...person,
[key]: value
};
}
return person;
});
// create a copy of the state
const newState = {
...colleagues,
persons: [...updatedPersons]
};
setColleagues(newState);
};
4) Is this valid, or do I need to destruct every part of my colleagues object separately?
const onChangeOtherValue = (key, value) => {
// for example changing the value foo
const newState = {
...colleagues,
[key]: value
};
setColleagues(newState);
};
I do have a feeling that only the concept of the first function is valid, while the rest of them are not.
Can this be done easily, or should I just use an immutable-helper?
Thanks in advance!
Updated examples to get syntax, right. Thanks Valerii.
To clarify
What I'm really after here is best practise to handle use cases like this one. I want to make sure my state is updated in the most correct and efficient way. So feel free to rip my examples a part or write new ones - I'm all ears. It is not necessary to simply modify mine to fit this post (unless they actually turn out to be good).
1) OK
2)
const updatedPersons = colleagues.persons.map(person => {
if(person.id === personId) {
return {
...person,
interests: person.interests.concat({ en: 'Test', es: 'Test' })
};
}
return person;
});
const newState = {
...colleagues,
persons: updatedPersons
};
3)
const updatedPersons = colleagues.persons.map(person => {
if(person.id === personId) {
return {
...person,
[key]: value
};
}
return person;
});
// create a copy of the state
const newState = {
...colleagues,
persons: updatedPersons
};
4) OK
for the first one, i would do this way,
const newState = {
...colleagues,
persons: [...persons, newColleague]
};

Resources