This question already has answers here:
Updating an object with setState in React
(23 answers)
Why can't I directly modify a component's state, really?
(7 answers)
How to update nested state properties in React
(36 answers)
Closed 12 months ago.
My react state is changing without me calling my setState function from useState hook.
After some quick research, i've narrowed it down to the fact an array is a reference type so data and tempData share the same reference and change with each other. A solution I found was to stringify then parse the data: JSON.parse(JSON.stringify(data)) but this could have some pretty bad performance hits if it's a large object right? Is there a better solution here? Redux? Or is that unnecessary? This is a pretty common case isn't it?
For anyone who cares, this works too but is kinda ugly:
change state to object rather than array
const defaultData = {
data: [
{id:0, foo:1, bar:2},
{id:1, foo:3, bar:4},
{id:2, foo:4, bar:6},
]
}
const handleData = (id) => {
setData((prevState) => {
return {data: data.data.map((i) => i.id === id ? {...i, id:i.id+10} : {...i})}
})
}
I've attached an example below which can be easily created from create-react-app.
App.js
import Child from './Child';
import { useState } from 'react';
const defaultData = [
{id:0, foo:1, bar:2},
{id:1, foo:3, bar:4},
{id:2, foo:4, bar:6},
]
function App() {
const [data, setData] = useState(defaultData)
const handleData = (id) => {
const tempData = data
for (const idx in tempData) {
const item = tempData[idx]
if (item.id === id) {
tempData[idx].id += 10
}
}
}
return (
<div>
<Child data={data} handleData={handleData} />
</div>
);
}
export default App;
Child.js
export default function Child(props) {
const {data, handleData} = props
return (
<div>
<ul>
{data.map((i) => (
<li key={i.id}>
<button onClick={() => handleData(i.id)}>
{i.foo} {i.bar}
</button>
</li>
))}
</ul>
</div>
)
}
Related
I have some files that builds a cart in a dropdown for my shop website.
One file adds the selected item to an array which will be my cart. The other file is the CartDropdown component itself. My cart only show the items when I close and open it (remounting), but I want it to remount every time I add a new item.
Adding item function:
const ProductContainer = ({ productInfo }) => {
const { cartProducts, setCartProducts } = useContext(CartContext);
const cartArray = cartProducts;
const addProduct = () => {
productInfo.quantity = 1;
if (cartArray.includes(productInfo)) {
const index = cartArray.findIndex((object) => {
return object === productInfo;
});
cartProducts[index].quantity++;
setCartProducts(cartArray);
} else {
cartArray.push(productInfo);
setCartProducts(cartArray);
}
// setCartProducts(cartArray)
console.log(cartProducts);
// console.log(cartArray)
};
};
dropdown component
const CartDropdown = () => {
const { setCartProducts, cartProducts } = useContext(CartContext);
const { setProducts, currentProducts } = useContext(ProductsContext);
// useEffect(() => {}, [cartProducts])
const cleanCart = () => {
const cleanProducts = currentProducts;
console.log(cleanProducts);
for (let i in cleanProducts) {
if (cleanProducts[i].hasOwnProperty("quantity")) {
cleanProducts[i].quantity = 0;
}
}
setProducts(cleanProducts);
setCartProducts([]);
};
return (
<div className="cart-dropdown-container">
<div className="cart-items">
{cartProducts.map((product) => (
<div key={product.id}>
<img src={product.imageUrl}></img>
</div>
))}
</div>
<button onClick={cleanCart}>CLEAN CART</button>
<Button children={"FINALIZE PURCHASE"} />
</div>
);
};
How can I force the dropdown to remount every time cartProducts changes?
CART CONTEXT:
export const CartContext = createContext({
isCartOpen: false,
setIsCartOpen: () => { },
cartProducts: [],
setCartProducts: () => { }
})
export const CartProvider = ({ children }) => {
const [isCartOpen, setIsCartOpen] = useState(false)
const [cartProducts, setCartProducts] = useState([])
const value = { isCartOpen, setIsCartOpen, cartProducts, setCartProducts };
return (
<CartContext.Provider value={value}>{children}</CartContext.Provider>
)
}
product context
export const ProductsContext = createContext({
currentProducts: null,
setProducts: () => {}
})
export const ProductsProvider = ({children}) => {
const [currentProducts, setProducts] = useState(shop_data)
const value = {currentProducts, setProducts}
return(
<ProductsContext.Provider value={value}>{children}</ProductsContext.Provider>
)
}
You can change the key prop of the component every time you want to remount. Every time cartProduct changes, update the value of key. You can do that using a useEffect with cartProduct as a dependency.
<CartDropdown key={1} />
to
<CartDropdown key={2} />
Edit for more clarification:
const [keyCount, setKeyCount] = useState(0);
useEffect(() => {
setKeyCount(keyCount+1);
}, [cartProducts]);
<CartDropdown {...otherProps} key={keyCount} />
The first issue I see is that you are not using the callback to set the state inside the context but you are doing cartProducts[index].quantity++ and react docs specify
Do Not Modify State Directly
Also after cartProducts[index].quantity++, you call setCartProducts(cartArray); not with cartProducts which you actually updated (this is also the reason why "if I do usestate(console.log('A'), [cartProducts]) its not triggering everytime i add my cart product". But anyway there is an issue even if you would use cartArray for both:
You shouldn't directly do const cartArray = cartProducts since by doing so cartArray will be a reference to cartProducts (not a copy of it) which also shouldn't be modified (because it would mean that you are modifying state directly).
So first 2 things I recommend you to improve would be:
Initialize cartArray as a cartProducts deep copy (if your cartProducts is an array of objects, spread syntax won't do it). So I would reccomand you to check this question answers for creating a deep copy.
After you make sure that cartArray is a deep copy of cartProducts, doublecheck you use cartArray to create a local newValue then set the state of the context with the same value (so basically:
cartArray[index].quantity++;
setCartProducts(cartArray);
)
The deep copy part also apply for const cleanProducts = currentProducts; (you should also create a deep copy here for cleanProducts, instead of saving the object ref).
If you are not using deep copies, your code might still work in some cases, but you might encounter weird behaviors in some other instances (and thoose are really hard to debug). Therefore is a bad practice in general not using deep copies.
How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};
I'm new to react and only understand the basics. I got this project from someone to look at, but I'm scratching my head since morning with this problem:
Uncaught TypeError: this.state.persons.map is not a function.
Please, if you can try to try to go over it in easy but in under the hood way. Thank You!
import React, { useState, Component } from 'react';
import './App.css';
import Person from './Person/Person';
import person from './Person/Person';
import { render } from '#testing-library/react';
class App extends Component {
state =
{
persons:[
{id: '123', name:'Max', age: 28 },
{id: '124',name:'Mari', age: 26 },
{id: '125',name: 'laly', age: 20 }
],
showPersons: false
}
nameChangeHandler=( event,id ) =>
{ const personIndex = this.state.persons.findIndex(p=>{
return p.id === id;
});
const person = {...this.state.persons[personIndex]
};
person.name=event.target.value;
const persons=[ ...this.state.persons];
persons[personIndex]=person;
this.setState(
{
persons:person
}
)
}
togglePersonHandler = ()=>
{
const doesShow = this.state.showPersons;
this.setState ({showPersons: !doesShow});
}
deletePersonHandler= (personIndex)=> {
//const persons = this.state.persons;
const persons = [...this.state.persons]
persons.splice(personIndex,1);
this.setState({persons:persons});
}
render()
{
const style ={
backgroundColor:'yellow',
font:'inherit',
border:'1px solid blue',
padding:'8px',
cursor:'pointer'
};
let persons=null;
if (this.state.showPersons){
persons= (
<div>
{this.state.persons.map((person,index)=> {return <Person click={() => this.deletePersonHandler(index)}
name = {person.name}
age = {person.age}
key={person.id}
change ={(event) => this.nameChangeHandler(event,person.id)}
/>
})};
</div>)
};
return (
<div className="App">
<h1>Hi This is react App</h1>
<button style={style} onClick={this.togglePersonHandler}> Toggle Persons</button>
{persons}
</div>
);
}
}
export default App;
Error lies here :
nameChangeHandler=( event,id ) =>
{ const personIndex = this.state.persons.findIndex(p=>{
return p.id === id;
});
const person = {...this.state.persons[personIndex]
};
person.name=event.target.value;
const persons=[ ...this.state.persons];
persons[personIndex]=person;
this.setState(
{
persons:person ------------ > You are assigning a single object to a list in your state, so your map is giving an error, it must be {persons: persons}
}
)
}
Whenever you see ___ is not a function, try looking at whatever the function is referring to.
In this case you're using map, which is an Array function. So you need to verify if the array (the thing at the left of the dot) is actually an array.
The function call:
this.state.persons.map
The array you need to pay attention to is persons. So try to look for the place in the code where persons is not getting recognized an array.
You have a typo here, you are assigning and object to state instead of an array. in the funcion nameChangeHandler.
this.setState(
{
persons:person
}
)
With a simple console inside the render or just watching it inside the Components Menu in the browser you can notice it bro.
It probably happens here, where you replace the array of persons with a single person:
this.setState(
{
persons: person
}
)
You probably want to do this instead:
this.setState({persons});
As per the docs:
When the nearest <MyContext.Provider> above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo or shouldComponentUpdate, a rerender will still happen starting at the component itself using useContext.
...
A component calling useContext will always re-render when the context value changes.
In my Gatsby JS project I define my Context as such:
Context.js
import React from "react"
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {},
}
const Context = React.createContext(defaultContextValue)
class ContextProviderComponent extends React.Component {
constructor() {
super()
this.setData = this.setData.bind(this)
this.state = {
...defaultContextValue,
set: this.setData,
}
}
setData(newData) {
this.setState(state => ({
data: {
...state.data,
...newData,
},
}))
}
render() {
return <Context.Provider value={this.state}>{this.props.children}</Context.Provider>
}
}
export { Context as default, ContextProviderComponent }
In a layout.js file that wraps around several components I place the context provider:
Layout.js:
import React from 'react'
import { ContextProviderComponent } from '../../context'
const Layout = ({children}) => {
return(
<React.Fragment>
<ContextProviderComponent>
{children}
</ContextProviderComponent>
</React.Fragment>
)
}
And in the component that I wish to consume the context in:
import React, { useContext } from 'react'
import Context from '../../../context'
const Visuals = () => {
const filterByYear = 'year'
const filterByTheme = 'theme'
const value = useContext(Context)
const { filterBy, isOptionClicked, filterValue } = value.data
const data = <<returns some data from backend>>
const works = filterBy === filterByYear ?
data.nodes.filter(node => node.year === filterValue)
:
data.nodes.filter(node => node.category === filterValue)
return (
<Layout noFooter="true">
<Context.Consumer>
{({ data, set }) => (
<div onClick={() => set( { filterBy: 'theme' })}>
{ data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1> }
</div>
)
</Context.Consumer>
</Layout>
)
Context.Consumer works properly in that it successfully updates and reflects changes to the context. However as seen in the code, I would like to have access to updated context values in other parts of the component i.e outside the return function where Context.Consumer is used exclusively. I assumed using the useContext hook would help with this as my component would be re-rendered with new values from context every time the div is clicked - however this is not the case. Any help figuring out why this is would be appreciated.
TL;DR: <Context.Consumer> updates and reflects changes to the context from child component, useContext does not although the component needs it to.
UPDATE:
I have now figured out that useContext will read from the default context value passed to createContext and will essentially operate independently of Context.Provider. That is what is happening here, Context.Provider includes a method that modifies state whereas the default context value does not. My challenge now is figuring out a way to include a function in the default context value that can modify other properties of that value. As it stands:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {}
}
set is an empty function which is defined in the ContextProviderComponent (see above). How can I (if possible) define it directly in the context value so that:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
test: 'hi',
set: (newData) => {
//directly modify defaultContextValue.data with newData
}
}
There is no need for you to use both <Context.Consumer> and the useContext hook.
By using the useContext hook you are getting access to the value stored in Context.
Regarding your specific example, a better way to consume the Context within your Visuals component would be as follows:
import React, { useContext } from "react";
import Context from "./context";
const Visuals = () => {
const filterByYear = "year";
const filterByTheme = "theme";
const { data, set } = useContext(Context);
const { filterBy, isOptionClicked, filterValue } = data;
const works =
filterBy === filterByYear
? "filter nodes by year"
: "filter nodes by theme";
return (
<div noFooter="true">
<div>
{data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1>}
the value for the 'works' variable is: {works}
<button onClick={() => set({ filterBy: "theme" })}>
Filter by theme
</button>
<button onClick={() => set({ filterBy: "year" })}>
Filter by year
</button>
</div>
</div>
);
};
export default Visuals;
Also, it seems that you are not using the works variable in your component which could be another reason for you not getting the desired results.
You can view a working example with the above implementation of useContext that is somewhat similar to your example in this sandbox
hope this helps.
Problem was embarrassingly simple - <Visuals> was higher up in the component tree than <Layout was for some reason I'm still trying to work out. Marking Itai's answer as correct because it came closest to figuring things out giving the circumstances
In addition to the solution cited by Itai, I believe my problem can help other people here
In my case I found something that had already happened to me, but that now presented itself with this other symptom, of not re-rendering the views that depend on a state stored in a context.
This is because there is a difference in dates between the host and the device. Explained here: https://github.com/facebook/react-native/issues/27008#issuecomment-592048282
And that has to do with the other symptom that I found earlier: https://stackoverflow.com/a/63800388/10947848
To solve this problem, just follow the steps in the first link, or if you find it necessary to just disable the debug mode
I have a functional component in which I have a button. I want to print a JSON string on click of that button. But whenever I try to return something it gives an error.
My component:
const emp_details= (props) => {
function getdata(data){
//console.log('button clicked');
console.log(data);
let result = data.reduce((r,c) =>
(r[c.company_code] = [...(r[c.company_code] || []), c.emp_code]) && r, {})
const json = {emp_details : result};
//console.log(JSON.stringify(json))
return JSON.stringify(json);
}
return (
<div>
<button onClick= {getdata(props.details)}>Display </button>
</div>
);
};
export default ShoppingCart;
I get this error:
Invariant Violation: Expected onClick listener to be a function, instead got a value of string type.
How should I display the JSON result?
I expect to see an output on the screen not on the console. So, after I click the button, it should display the output inside a tag or something.
emp_details: {
//json string of the state
}
This happens because you are invoking your getdata() as soon as the component renders. To work around this you want to pass an anonymous function to your event-listener which will call this function when the event actually occurs.
<div>
<button onClick= {() => getdata(props.details)}>Display</button>
</div>
To get the data to display inside the component after clicking the button, we need to employ some sort of state-management to force the component to re-render.
Let's consider the following-code:
Index.js
import React from "react";
import ReactDOM from "react-dom";
import { useState } from "react";
import ShoppingCart from "./ShoppingCart";
const details = [
{
emp_code: "a001",
company_code: "company_a",
name: "abx",
details: [],
details_dtypes: []
},
{
emp_code: "b002",
company_code: "company_b",
name: "xbz ",
details: [],
details_dtypes: []
},
{
emp_code: "a002",
company_code: "company_a",
name: "xbz ",
details: [],
details_dtypes: []
},
{
emp_code: "b003",
company_code: "company_b",
name: "xbz ",
details: [],
details_dtypes: []
}
];
const App = () => {
return (
<div>
<ShoppingCart details={details} />
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
So in the code above we pass in data as property called details to ShoppingCart.
ShoppingCart.js
import React from "react";
import { useState } from "react";
const ShoppingCart = props => {
//this gives us a state-value and state-updating function in that order. We passed in a default value of ""
const [companies, setCompanies] = useState({});
function getdata(data) {
let result = data.reduce(
(r, c) =>
(r[c.company_code] = [...(r[c.company_code] || []), c.emp_code]) && r,
{}
);
console.log(result);
//create our data object and then update our state-value, forcing our component to re-render
setCompanies(result);
}
//this creates a mark-up. It will get called again when we get an updated companies state value.
const createMarkup = () => {
//we're going to use the updated companies state-value now
let markup = Object.entries(companies).map(
([companyName, array], index) => {
return (
<div key={index}>
<p>{companyName}:</p>
{array.map((emp, empIndex) => {
return <p key={empIndex}>{emp}</p>;
})}
</div>
);
}
);
return markup;
};
return (
//createMarkup() will be executed on re-render to display our markup
<div>
<div>{createMarkup()}</div>
<button onClick={() => getdata(props.details)}>Display </button>
</div>
);
};
export default ShoppingCart;
We pass in the data stored in props.detail as the argument for getData(). getData() will parse it to the structure we need and give us a result back. Then we store that result in our hook-state.
When the component re-renders we call createMarkup() thus giving us the JSX that includes all our companies and employee information.
You are setting onClick not to the function getdata but the value returned by the function getdata.
Use a wrapper function all call function in that when you need pass arguments to function.
<button onClick= {() => getdata(props.details)}>Display </button>
function Emp_details(props) {
function getdata(){
console.log(props.details);
}
return <div><button onClick= {getdata}>Display </button></div>
}
ReactDOM.render(<Emp_details details = "John Doe"/>, document.getElementById('root'));