I have an array of objects in a useState hook with which i rendered input elements.
The state doesn't get updated when the input element changes.
Here Is My Code.
ProceduralResponseAnswerCreate Component
const ProceduralResponseAnswerCreate = () => {
const [proceduralAnswers, setProceduralAnswers] = useState([{ "value": "One" }, { "value": "two" }])
return <>
<ol>
{proceduralAnswers.map((answer, answer_index) => <input key={answer_index} style={inputStyle} onChange={(event) => updateAnswerValue({ event, setProceduralAnswers, answer_index })} value={answer.value} />)}
</ol>
</>
}
export default ProceduralResponseAnswerCreate
updateAnswerValue function
export const updateAnswerValue = ({ event, setProceduralAnswers, answer_index }) => {
setProceduralAnswers(state => {
var newState = state;
var currentAnswer = newState.filter((state_1, index) => index === answer_index)[0]
currentAnswer.value = event.target.value
return newState
})
}
I think it doesn't work because the next state has the same Reference as the previous state, you only change the value of one element. The useState hook will trigger a rerender, if the reference changes. So your Callback must look like this.
setProceduralAnswers(state => {
let newState = state;
let currentAnswer = newState.filter((state_1, index) => index === answer_index)[0]
currentAnswer.value = event.target.value
return [...newState]
})
try this
export const updateAnswerValue = ({ event, setProceduralAnswers, answer_index }) => {
setProceduralAnswers(state => {
var newState = state;
newState[answer_index].value = event.target.value
return newState
})
}
You are returning the newState instead of the currentAnswer.
First off, if you're only trying to find one item use [].find not filter.
var newState = state; does not give you a fresh copy, change newState, and you'll be changing state too, you need to do var newState = [...state]; (warning: this is a shallow copy only).
It looks like you think currentAnswer shares a reference with newState in some way, which it doesn't. Whenever you run map, find, filter or reduce you're getting a fresh copy of the array so there will not be a link between the 2.
A good bet here is to just map the state
export const updateAnswerValue = ({
event,
setProceduralAnswers,
answer_index
}) => {
setProceduralAnswers((state) => state.map((state_1, index) => {
if (index === answer_index) {
return {
...state_1,
value: event.target.value
}
}
return state_1;
}));
};
Related
I have a warning on this nested setState pointing a "bad setState call" because I am calling a setState inside a setState, so I understand I must avoid that and take out the children setState from there, the problem is that the children is using the parent prevState for a conditional so I don't know how to solve this.
This is my code:
const setStateImageAndIndex = (state, image) => {
setSketchState(prevState => {
if (state.index !== undefined && prevState.index !== state.index) {
setCurrentSketchIndex(state.index);
}
const new_state = {
...prevState,
...state,
image: image
};
return new_state;
});
};
The warning is pointing the setCurrentSketchIndex(state.index); explicitly, which is the function I want to take out from setSketchState. Thank you!
prevState in setSketchState infact is the actual state of sketchState (I mean the state setted by setSketchState). So you could write something like:
const setStateImageAndIndex = (state, image) => {
if (state.index !== undefined && sketchState.index !== state.index) {
setCurrentSketchIndex(state.index);
}
setSketchState(prevState => {
const new_state = {
...prevState,
...state,
image: image
};
return new_state;
});
};
you can hold the previous state of the parent component in a useRef and derivate the state in the child component passing that value down as a prop, for example:
function Parent() {
let [state, setState] = React.useState({ version: 1 });
let prevState = usePrevious(state.version);
return (
<div>
<p>Parent value: {state.version}</p>
<Child value={prevState} />
<button
onClick={() =>
setState({
version: state.version + 1
})
}
>
Update version
</button>
</div>
);
}
function Child(props) {
return (
<div>
<p>Child value: {props.value || "No value yet"}</p>
</div>
);
}
as we can see above, the parent component will update the state.version in the button click and usePrevious will hold the previous value:
function usePrevious(value) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
}
Working example in: https://codesandbox.io/s/use-previous-hook-0iemv
According to the comments above from #GabrielePetrioli and #GiovanniEsposito, this solution seemed to be safer.
const setStateImageAndIndex = (state, image) => {
let updateCurrentSketchIndex = false;
setSketchState(prevState => {
if (state.index !== undefined && prevState.index !== state.index) {
updateCurrentSketchIndex = true;
}
const new_state = {
...prevState,
...state,
image: image
};
return new_state;
});
if (updateCurrentSketchIndex) {
setCurrentSketchIndex(state.index);
}
};
I'm trying to understand how make a component that can remove itself from a array of components with functional components. Here is the sample code of what I'm trying to do:
const App = () => {
<ObjState>
<ObjectCreator />
<ObjectList />
</ObjState>
}
const ObjContext = createContext();
const ObjReducer = (state, { type, payload }) => {
switch(type) {
case Types.ADD_OBJ:
return {
...state,
objects: [...state.objects, payload]
};
case Types.REMOVE_OBJ:
return {
...state,
objects: state.objects.filter(obj => obj !== payload)
};
default:
return state;
}
}
const ObjState = ({ children }) => {
const initialState = {
objects: []
}
const [state, dispatch] = useReducer(ObjRecuder, initialState);
const addObj = (obj) => {
dispatch({
type: Types.ADD_OBJ,
payload: obj
});
}
const removeObj = (obj) => {
dispatch({
type: Types.REMOVE_OBJ,
payload: obj
});
}
return (
<ObjContext.Provider value={{
objects: state.objects,
addObj,
removeObj
}}>
{children}
</ObjContext.Provider>
);
}
const ObjCreator = () => {
const { addObject } = useContext(ObjContext);
const createObj =() => {
const obj = (<ObjectTypeA key={uuid()} />);
addObject(obj);
}
return (<button onClick={createObj}>create an object!</button>)
}
const ObjectList = () => {
const { objects } = useContext(ObjContext)
return (
<fragment>
{objects}
</fragment>
)
}
const ObjectTypeA = ({ key }) => {
const { removeObj } = useContext(ObjContext);
const removeSelf = () => {
removeObj(this);
}
return (
<button onClick={removeSelf}>remove me!</button>
)
}
The problem is you can't reference this in the final Object component.
I have the unique key but I'm not sure how to pass it through correctly. I attempted to build a reducer action that took the key from the Object and removed it that way but key came back as undefined even though it is deconstructed out of the props and I'm using an arrow function to preserve it.
I feel like I'm tackling this problem in the wrong way.
Issue
I think you veer off-course when trying to store what looks to be React components in your context state, you should be storing objects instead. The objects should have unique GUIDs. This allows the reducer to identify which object element to remove from state. The ObjectList should then render derived React components from the stored state.
I attempted to build a reducer action that took the key from the
Object and removed it that way but key came back as undefined even
though it is deconstructed out of the props and I'm using an arrow
function to preserve it.
This is because React keys (and refs) are not actually props. Keys can't be accessed in children components. You can can pass the same value via just about any other named prop though. Note below in solution I pass a React key and an id prop.
Solution
ObjectCreator: Creates objects, not React components
const ObjectCreator = () => {
const { addObj } = useContext(ObjContext);
const createObj = () => {
const obj = {
id: uuid()
};
addObj(obj);
};
return <button onClick={createObj}>create an object!</button>;
};
SpecificObject: passes its id to the removeObj callback.
const MyObject = ({ id }) => {
const { removeObj } = useContext(ObjContext);
const removeSelf = () => {
removeObj(id);
};
return (
<div>
<button onClick={removeSelf}>remove {id}</button>
</div>
);
};
ObjectList: renders the context objects mapped to JSX.
const ObjectList = () => {
const { objects } = useContext(ObjContext);
return (
<>
{objects.map((el) => (
<MyObject key={el.id} id={el.id} />
))}
</>
);
};
Check the passed id payload in the remove object reducer
const ObjReducer = (state, { type, payload }) => {
switch (type) {
case Types.ADD_OBJ:
return {
...state,
objects: [...state.objects, payload]
};
case Types.REMOVE_OBJ:
return {
...state,
objects: state.objects.filter((obj) => obj.id !== payload)
};
default:
return state;
}
};
Demo
console.log for item.completion is triggered twice. It displays the new inverted value and switch back to its original value. So, every time toggleTask function is called the item.completion is not changed. What causes this?
toggleTask(id) {
this.setState(prevState => {
const newTodos = prevState.todos.map(item => {
if (item.id === id) {
item.completion = !item.completion
console.log(item.completion)
}
return item
})
return { todos: newTodos }
})
}
The problem is here,
this.setState(prevState => {
const newTodos = prevState.todos.map(item => {
if (item.id === id) {
item.completion = !item.completion // !! HERE !!
console.log(item.completion)
}
return item
})
return { todos: newTodos }
})
AS, Object are passed by reference not by value, so when your modifying it the state is getting directly modified and React sees no diff from your return and the state, thus no render takes place.
try something that doesn't mod the object directly, like this,
this.setState(prevState => {
const newTodos = prevState.todos.map(_item => {
const item = Object.create(_item); // NEW
if (item.id === id) {
item.completion = !item.completion
console.log(item.completion)
}
return item
})
return { todos: newTodos }
})
inspired from JavaScript: How to pass object by value?
I have a condition given below where I want to update an array of objects present in state, although the state is changing but not rendering, maybe somehow the state is being directly mutated, or any other reason. I'am unable to figure out what am I doing wrong
handleCondition = e => {
const { value, id } = e.target;
this.setState(
({ newData }) => {
const testData = newData;
testData.find(data => data.variable === id.split('_')[1])[id.split('_')[0]] = value;
return ({
newData: testData
});
},
() =>
localStorage.setItem(
"newData",
JSON.stringify(this.state.newData)
)
);
};
render(){
return(
<select
id={`wall_${data.variable}`}
className="custom-slt-home"
onChange={this.handleCondition}
value={data.wall}
>
<option value="paint">Paint</option>
<option value="wallpaper">Wallpaper</option>
</select>
)
In this line:
testData.find(data => data.variable === id.split('_')[1])[id.split('_')[0]] = value;
You don't mutate an array, so as a result you're returning to the setState the same value.
Try this instead:
const itemIndex = newData.findInex(data => data.variable === id.split('_')[1]);
const item = [...newData[itemIndex]];
item[id.split('_')[0] = value;
return ({
newData: [...newData.slice(0, itemIndex), item, ...newData.slice(itemIndex + 1)];
});
There may be some issues, because you didn't provide any info about your data structure. But I hope you'll understand the point.
I have stored an array of object in Redux State and inside each object there is a key named price. Now when I increment the quantity button I need to access the object that has the key inside redux and change the price value. I was able to do that but it's not working properly the price is being changed but a new object is being added in the state of Redux you can see it in the screenshot below. hope I was able to explain the problem clearly. if not please let know so I can explain more.
Cart Component
increment(e, item){
let qty = e.target.previousElementSibling.textContent;
qty++;
e.target.previousElementSibling.textContent = qty;
this.props.changePrice(item);
}
<div>
<input onClick={(e) =>this.decrement(e)} type="submit" value="-"/>
<span>1</span>
<input onClick={(e) => this.increment(e, item)} type="submit" value="+"/>
</div>
function mapStateToProps(state){
return({
itemlist: state.rootReducer
})
}
function mapDispatchToProps(dispatch) {
return({
removeItem: (item)=>{
dispatch({type: 'removeCart', payload: item})
},
changePrice: (item)=>{
dispatch({type: 'changePrice', payload: item})
}
})
}
export default connect(mapStateToProps, mapDispatchToProps)(Cart);
Reducer Component
const changePrice = (itemArray, item)=>{
let newObject = {};
let filteredObject = itemArray.filter(reduxItem => reduxItem.id === item.id);
let newprice = filteredObject[0].price + filteredObject[0].price;
filteredObject[0].price = newprice;
newObject = filteredObject;
const something = ([...itemArray, newObject]);
return something;
}
const reducer = (state = [], action) =>{
switch (action.type) {
case 'Add':
return [...state, action.payload]
case 'removeCart':
const targetItemIndex = state.indexOf(action.payload);
return state.filter((item, index) => index !== targetItemIndex)
case 'changePrice':
return changePrice(state, action.payload)
default:
return state;
}
}
export default reducer;
filteredObject is an array. You override the newObject to be an array in this statement newObject = filteredObject. So the newObject is an array ( in [...itemArray, newObject] ) rather than an object. Keep things simple without unnecessary complexity.You can use Array.map. So do this instead
const changePrice = (itemArray, item) => {
return itemArray.map(reduxItem => {
if(reduxItem.id === item.id){
reduxItem.price = reduxItem.price + reduxItem.price
}
return reduxItem
});
};
See this for more info https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns#inserting-and-removing-items-in-arrays
Hope this helps!
Instead of mutating the state.
// use this
const newState = Object.assign({},state);
We can create a new state and now if you do this, this works fine.
This avoids mutating state.