useState hook not "reacting" when state variable modified - reactjs

I have created a React application to test out the useState hook.
This the variable of concern:
let [blocks, setBlocks] = useState([
{text: "Hello", id: 1},
{text: "This is google", id: 2},
{text: "Wassup", id: 3},
{text: "Last One", id: 4}
]);
I've displayed this using the map function as follows:
return (
<div className="App">
{blocks.map((block) => (
<div className='block-element'>{block.text}</div>
))}
<button onClick={clickFunc}>ClickToChange</button>
</div>
);
As far as I've understood, to make any change in the webpage we have to pass the new-data into setBlocks() and wherever "blocks" was used will be updated.
I tried the following clickFunc() to do so:
const clickFunc = ()=>{
blocks[1].text = "Go Home";
setBlocks(blocks);
console.log(blocks);
}
I expected the output (onclicking the button) to be:
Hello
Go Home
Wassup
Last One
But nothing changed.
Surprisingly when I used the following (similar looking) clickFunc():
const clickFunc = ()=>{
blocks = [
{ text: "Hello", id: 1 },
{ text: "Go Home", id: 2 },
{ text: "Wassup", id: 3 },
{ text: "Last One", id: 4 }
];
setBlocks(blocks);
console.log(blocks);
}
And it worked perfectly as expected.
On click output:
Hello
Go Home
Wassup
Last One

NEVER mutate state directly.
The following code block is mutation and wrong. Javascript is kind of like some other programming languages where only the reference of the object is stored in the memory. So when you mutate the object instead of creating a new one, you are not really changing the content of the object in memory, because it still has the same value(reference) as before.
const blocks = [{text: 'one'}];
blocks[0].text = 'two';
You should create a new object and assign it to state using React setState callback:
setBlocks(blocks => {
clonedBlocks = [ ...blocks];
clonedBlocks[1]= { ...blocks[1], text: "Go Home" };
return clonedBlocks;
});
Or any other way that does not mutate the state directly.

React state is immutable
if you mutate the state react will not know that state is changed
first you need to copy the state using Spread syntax and change it
const clickFunc = () => {
setBlocks(prevBlocks => {
const shallowCopy = [...prevBlocks]
shallowCopy[1].text = "Go Home";
return shallowCopy
});
}

Related

No using class Component , only function in React

Hello I am just learning react and I am looking at tutorials but in the version that was installed, react is no longer using the classes, it only appears functions and I would like to continue that way if possible, but I have a problem with where to change the name in this part with a click but it does not allow me to access persons(const), how could I do it?
import Person from './Person/Person'
function App() {
const persons = {
persons: [
{ name: 'Jose', age: 32},
{ name: 'Gabriel', age: 2}
]
}
const switchNameHandler = () => {
persons({
persons: [
{ name: 'Jose Fernando', age: 32},
{ name: 'Gabriel', age: 2}
]
})
}
return (
<div className="App">
<h1>Hi, I'm a React App!</h1>
<button onClick={switchNameHandler}> Switch Name</button>
<Person name={persons.persons[0].name} age={persons.persons[0].age}/>
<Person name={persons.persons[1].name} age={persons.persons[1].age}> I like play</Person>
</div>
);
}
export default App;
How could I fix the switchNameHandler part?
I know that if I use classes I can access this.setPersons, but is there any way to access without using classes?
You need to use the useState hook. All hooks return two things in a de-structured array (1) the state value and (2) the function to set that state value. and the value you put in the useState() function is the initial value.
For example:
const [name, setName] = useState("Ben");
Here the initial value of name is "Ben". If I wanted to change that value, I could use the setName function and do something like this setName("Jerry");. Now the value of name is "Jerry".
The biggest difference between the setter (in this case setName) and this.setState (in a class component), is that this.setState remembers and spreads the previous state automatically for you if you don't explicitly define it. With hooks, you have to do that your self:
For example:
const [person, setPerson] = useState({ name: "", id: "", age: "" });
If I have this state, and want to edit just the name of the person, I have to remember to spread the previous state object every time I update state, by using a callback function - where the parameter in the callback is the previous state:
// just updating name
setPerson(prevPerson => ({ ...prevPerson, name: "Bob" }));
Here, the only thing that changes was the "name" value and everything else stayed the same:
Result: { name: "Bob", id: "", age: ""}
Check out the react documentation for more tips and examples: https://reactjs.org/docs/hooks-state.html

Setting Nested Array in React Context

I am trying to figure out how to update an array that is nested within a React Context. Below is the context I am working with. It consists of "lists". Each list contains an array of "items".
import React, {useState, createContext} from 'react';
export const ListerContext = createContext();
export const ListerProvider = (props) => {
const [lists, setLists] = useState([
{
id: 1,
items: [{
itemid: 1,
text: 'Hello'
},
{
itemid: 2,
text: 'world'
}]
},
{
id: 2,
items: [{
itemid: 2,
text: 'Test'
}]
}
]);
return(
<ListerContext.Provider value={[lists, setLists]}>
{ props.children }
</ListerContext.Provider>
);
}
I have been trying to change the nested arrays using the "setLists" method below but it is not working. What am I doing wrong here?
const removeListItem = (e) => {
setLists((prevList)=>{
for(var i = 0; i < prevList.length; i++){
if(prevList[i].id === parseInt(e.target.value[2])){
prevList[i].items = (prevList[i].items.filter(function(item) {
return item.itemid !== parseInt(e.target.value[0]);
}))
}
}
return prevList;
});
}
As #zero298 mentioned in their comment, you need to pass the entire list into your state update function setLists. You are initializing the state and its update function with a "list of dictionaries", so you need to keep treating it as such, you can't selectively change few properties inside the dictionary or few elements in the list without having to update the entire list.
Easy correction of your code will be to conditionally make a new copy of your list with updated values, and then setting that list as your new state using your state update function.
Cheerio!

Issue with state update approach for nested objects

Major EDIT
I have quite huge object which is 3 level deep. I use it as a template to generate components on the page and to store the values which later are utilized, eg:
obj =
{
"group": {
"subgroup1": {
"value": {
"type": "c",
"values": []
},
"fields_information": {
"component_type": "table",
"table_headers": [
"label",
"size"
],
}
},
"subgroup2": {
"value": {
"type": "c",
"values": []
},
"fields_information": {
"component_type": "table",
"table_headers": [
"label",
"size"
],
}
},
},
}
Thanks to this I can dynamically generate view which is, as a template, stored in DB.
I'm struggling with 2 things. Firstly, updating values basing on user input for textbox, checkboxes and similar.
I'm doing it this way:
const updateObj = (group, subgroup, value) => {
let tempObj = {...obj}
tempObj[group][subgroup].value.value = value
toggleObj(tempObj)
}
I know that the spread operator is not in fact doing deep copy. However it allows me to work on the object and save it later. Is that an issue? Do I have to cloneDeep or it is just fine? Could cloneDeep impact performance?
Second case is described below
export const ObjectContext = React.createContext({
obj: {},
toggleObj: () => {},
});
export const Parent = (props) => {
const [obj, toggleObj] = useState()
const value = {obj, toggleObj}
return (
<FormCreator />
)
}
const FormCreator = ({ catalog }) => {
const {obj, toggleObj} = React.useContext(ObjectContext)
return (<>
{Object.keys(obj).map((sectionName, sectionIdx) => {
const objFieldsInformation = sectionContent[keyName].fields_information
const objValue = sectionContent[keyName].value
...
if (objFieldsInformation.component_type === 'table') {
return (
<CustomTable
key={keyName + "id"}
label={objFieldsInformation.label}
headers={objFieldsInformation.table_headers}
suggestedValues={[{label: "", size: ""}, {label: "", size: ""}, {label: "", size: ""}]}
values={objValue.values}
sectionName={sectionName}
keyName={keyName}/>
)
}
...
})}
</>)
}
const CustomTable= (props) => {
const { label = "", headers = [], suggestedValues = [], values, readOnly = false, sectionName, keyName } = props
const {obj, toggleObj} = React.useContext(ObjectContext)
//this one WORKS
useEffect(() => {
if (obj[sectionName][keyName].value.type === "complex") {
let temp = {...obj}
temp[sectionName][keyName].value.values = [...suggestedValues]
toggleObj(temp)
}
}, [])
//this one DOES NOT
useEffect(() => {
if (obj[sectionName][keyName].value.type === "c") {
let temp = {...obj, [sectionName]: {...obj[sectionName], [keyName]: {...obj[sectionName][keyName], value: {...obj[sectionName][keyName].value, values: [{label: "", size: ""}, {label: "", size: ""}, {label: "", size: ""}]}}}}
toggleObj(temp)
}
}, [])
return (
//draw the array
)
}
Please refer to CustomTable component.
As on the example Object above, I have 2 CustomTables to be printed. Unfortunately, one useEffect that should work is not working properly. I'm observing, that values field is set only for the last "table" in Obj. When I'm doing shallow copy of obj, it works fine. But I'm afraid of any repercussion that might happens in future.
I'm also totally new to using createContext and maybe somehow it is the issue.
Kudos to anyone understanding that chaos :)
The main issue appears to be that you are not providing your context. What you have is literally passing the blank object and void returning function. Hence why calling it has no actual effect, but mutating the value does.
export const ObjectContext = React.createContext({
obj: {},
toggleObj: () => {},
});
export const Parent = (props) => {
const [obj, toggleObj] = useState({})
const value = {obj, toggleObj}
return (
<ObjectContext.Provider value={value}>
<FormCreator />
</ObjectContext.Provider>
)
}
Ideally you would also make this component above wrap around FormCreator and render it as props.children instead. This is to prevent the entire sub-tree being rerendered every time toggleObj is called. See the first part of this tutorial to get an idea of the typical pattern.
As to the question about mutating state, it absolutely is important to keep state immutable in React - at least, if you are using useState or some kind of reducer. Bugs arising from state mutation come up all the time on Stack Overflow, so often in fact that I recently made a codesandbox which demonstrates some of the more common ones.
I also agree with #SamuliHakoniemi that a deeply nested object like this is actually better suited to the useReducer hook, and might even go one further and suggest that a proper state management library like Redux is needed here. It will allow you to subdivide reducers to target the fragments of state which actually update, which will help with the performance cost of deeply cloning state structure if or when it becomes an actual issue.

How to initiate state from another state (by copying without referencing)

I have a code as below
useEffect(() => {
{
updateLocalColumns();
}
}, []);
const updateLocalColumns = () => {
let templocalColumns = [...localColumns];
templocalColumns[0]..defaultSortOrder = "descend";
setlocalColumns(templocalColumns);
}
const orderContext = useContext(OrderContext);
const { data, columns } = orderContext;
const [localColumns, setlocalColumns] = useState([...columns]);
return (
<div>
<Table columns={columns} dataSource={data} />
{console.log(columns)}
</div>
)
'columns' is a state that is obtained by using hooks, I am trying to make a copy over local component that is 'localColumns" and thus modify from there without interfering the global 'columns'
I was expecting the original columns to remain as its original state, however when I print out, the original columns is the same as 'localColumns' which is the modified one.
Expected Output
localColumns: [
{
title: "Username",
dataIndex: "username",
key: "username",
defaultSortOrder: "descend"
}
]
columns: [
{
title: "Username",
dataIndex: "username",
key: "username",
}
]
Current Wrong Output
localColumns: [
{
title: "Username",
dataIndex: "username",
key: "username",
defaultSortOrder: "descend"
}
]
columns: [
{
title: "Username",
dataIndex: "username",
key: "username",
defaultSortOrder: "descend"
}
]
I am suspecting the problem is when I initate the state, I did a referencing.
The way I did copy was from this link and this link
I would try a simpler code:
//Import this class in the proper place
//import { clone } from 'ramda';
const orderContext = useContext(OrderContext);
const { data, columns } = orderContext;
let localColumns0 = clone(columns) //This is done to make a deep copy of "columns"
localColumns0[id]['defaultSortOrder']="descend"
const [localColumns, setlocalColumns] = useState(localColumns0);
return (
<div>
<Table columns={columns} dataSource={data} />
{console.log(columns)}
</div>
)
If you don't want only the copy of the state, maybe you need to look how to do the deep copy of an array, after you make a deep copy of the estate you can pass the state to your personal component with <PersonalCom {...deepCopyStateObj}/> and the PersonalCom need a constructor where you receive the props, like the following code.
constructor(props) {
super(props);
}
Or something equivalent in JSX syntax.
However make a deep copy of the state and pass the object to component don't look like, because you need to clone the state each time that your app refresh the UI, and react refresh very frequently the UI, for this reason, I suggest cloning the propriety that you need inside the personal component.

react how to prevent default on setState without event

Let's say you have a defined function containing a this.setState in a react component which is not fired by an event.
How can you do preventDefault() in order to keep the current scroll on the page?
I create a sandbox to illustrate this behaviour:
https://codesandbox.io/embed/damp-night-r92m5?fontsize=14&hidenavigation=1&theme=dark
When groups are defined and something fire the renderer, the page scroll on top. This does not happend id groups are not defined or contain an empty array...
How can I prevent this scrolling
First of all, I think your issue was nothing to do with event but your component is triggering render.
Based on your sandbox that you provided, you are actually declaring a new array each time the component renders. Meaning that React will assume that your [{ id: 1, content: "group1" }, { id: 2, content: "group2" }] is a new instance, even though all the items in the array is the same.
This line is causing your issue :
groups={[{ id: 1, content: "group1" }, { id: 2, content: "group2" }]}
Method 1: Move your groups variables into state
const [groups, setGroups] = useState([{ id: 1, content: "group1" }, { id: 2, content: "group2" }]);
This way React will not rerender your App until you call setState ( In this case, setGroups )
Method 2: Move your groups outside of your App function
const groups = [{ id: 1, content: "group1" }, { id: 2, content: "group2" }];
function App() {
... App Codes
}
In this way, React will not rerender your App since groups is not declaring within App.
Method 3: Using Memoization useMemo React Hook
const groups = useMemo(() => [{ id: 1, content: "group1" }, { id: 2, content: "group2" }], []);
The second argument in your useMemo function defines your dependencies array, setting it to empty array means that the value will never change. Hence React will not rerender your App.
In your render:
groups={groups}

Resources