If I use a number or string as default value, it rerenders the app after use of setVotes just like with setSelected. With array it does not work (only would display real renewed array after page is rerendered, as it is easy to check with setSelected button)
const App = (props) => {
const [votes, setVotes] = useState([0,0,0,0,0,0])
const [selected, setSelected] = useState(0)
const handlenext = () => {
setSelected(Math.floor(Math.random() * 6))
}
const handlevote = () => {
let newvotes=votes
newvotes[selected]+=1
setVotes(newvotes)
}
return (
<div>
<button onClick={handlenext}> next anecdot</button>
<p>{props.anecdotes[selected]}</p>
<button onClick={handlevote}> vote</button>
<p>votes {votes[selected]}</p>
</div>
)
}
const anecdotes = [
'I',
's',
'T',
'A',
'l',
'.'
]
try this instead
const handlevote = () => {
let newvotes= [...votes]
newvotes[selected]+=1
setVotes(newvotes)
}
You need go build a new array. Do not use old array. Should you const a=[…b]
You need to pass a new reference. Arrays and object are reference based, if you don't create a new one is the same reference and react don't go deep to compare differrence. Besides you are mutating state directly which is bad.
Create a new copy from the state as const newvotes= [... votes]. Now you can pass next state without mutating the original and passing a new reference as well
Related
I'm working on my first React project and I have the following problem.
How I want my code to work:
I add Items into an array accessible by context (context.items)
I want to run a useEffect function in a component, where the context.items are displayed, whenever the value changes
What I tried:
Listing the context (both context and context.items) as a dependency in the useEffect
this resulted in the component not updating when the values changed
Listing the context.items.length
this resulted in the component updating when the length of the array changed however, not when the values of individual items changed.
wraping the context in Object.values(context)
result was exactly what I wanted, except React is now Complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
Do you know any way to fix this React warning or a different way of running useEffect on context value changing?
Well, didn't want to add code hoping it would be some simple error on my side, but even with some answers I still wasn't able to fix this, so here it is, reduced in hope of simplifying.
Context component:
const NewOrder = createContext({
orderItems: [{
itemId: "",
name: "",
amount: 0,
more:[""]
}],
addOrderItem: (newOItem: OrderItem) => {},
removeOrderItem: (oItemId: string) => {},
removeAllOrderItems: () => {},
});
export const NewOrderProvider: React.FC = (props) => {
// state
const [orderList, setOrderList] = useState<OrderItem[]>([]);
const context = {
orderItems: orderList,
addOrderItem: addOItemHandler,
removeOrderItem: removeOItemHandler,
removeAllOrderItems: removeAllOItemsHandler,
};
// handlers
function addOItemHandler(newOItem: OrderItem) {
setOrderList((prevOrderList: OrderItem[]) => {
prevOrderList.unshift(newOItem);
return prevOrderList;
});
}
function removeOItemHandler(oItemId: string) {
setOrderList((prevOrderList: OrderItem[]) => {
const itemToDeleteIndex = prevOrderList.findIndex((item: OrderItem) => item.itemId === oItemId);
console.log(itemToDeleteIndex);
prevOrderList.splice(itemToDeleteIndex, 1);
return prevOrderList;
});
}
function removeAllOItemsHandler() {
setOrderList([]);
}
return <NewOrder.Provider value={context}>{props.children}</NewOrder.Provider>;
};
export default NewOrder;
the component (a modal actually) displaying the data:
const OrderMenu: React.FC<{ isOpen: boolean; hideModal: Function }> = (
props
) => {
const NewOrderContext = useContext(NewOrder);
useEffect(() => {
if (NewOrderContext.orderItems.length > 0) {
const oItems: JSX.Element[] = [];
NewOrderContext.orderItems.forEach((item) => {
const fullItem = {
itemId:item.itemId,
name: item.name,
amount: item.amount,
more: item.more,
};
oItems.push(
<OItem item={fullItem} editItem={() => editItem(item.itemId)} key={item.itemId} />
);
});
setContent(<div>{oItems}</div>);
} else {
exit();
}
}, [NewOrderContext.orderItems.length, props.isOpen]);
some comments to the code:
it's actually done in Type Script, that involves some extra syntax
-content (and set Content)is a state which is then part of return value so some parts can be set dynamically
-exit is a function closing the modal, also why props.is Open is included
with this .length extension the modal displays changes when i remove an item from the list, however, not when I modify it not changeing the length of the orderItems,but only values of one of the objects inside of it.
as i mentioned before, i found some answers where they say i should set the dependency like this: ...Object.values(<contextVariable>) which technically works, but results in react complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
the values displayed change to correct values when i close and reopen the modal, changing props.isOpen indicating that the problem lies in the context dependency
You can start by creating your app context as below, I will be using an example of a shopping cart
import * as React from "react"
const AppContext = React.createContext({
cart:[]
});
const AppContextProvider = (props) => {
const [cart,setCart] = React.useState([])
const addCartItem = (newItem)=>{
let updatedCart = [...cart];
updatedCart.push(newItem)
setCart(updatedCart)
}
return <AppContext.Provider value={{
cart
}}>{props.children}</AppContext.Provider>;
};
const useAppContext = () => React.useContext(AppContext);
export { AppContextProvider, useAppContext };
Then you consume the app context anywhere in the app as below, whenever the length of the cart changes you be notified in the shopping cart
import * as React from "react";
import { useAppContext } from "../../context/app,context";
const ShoppingCart: React.FC = () => {
const appContext = useAppContext();
React.useEffect(() => {
console.log(appContext.cart.length);
}, [appContext.cart]);
return <div>{appContext.cart.length}</div>;
};
export default ShoppingCart;
You can try passing the context variable to useEffect dependency array and inside useEffect body perform a check to see if the value is not null for example.
recently I started to work in a proyect and I notice that I need some way to modify a value without losing my previous state. Basically work with an array. However I don't know how to do that.
Can anyone help me by telling me how can I modify an state (I'm using react hooks by the way...) to add more values to the previous state in an array?
Sorry not posting code or something like that, I don't even know how to write that.
Thank you in advance.
Use ES6 spread operator to push new object to the existing array.
Adding a new object to an Array:
const [todo, setTodo] = useState([{ task: "Todo 1" }]);
const addTodo = () => {
let newTodoTask = { task: `Task ${todo.length + 1}` };
setTodo(tasks => [...tasks, { ...newTodoTask }]);
};
Modifying an object in an Array:
const editTask = (e, taskId = 0) => {
setTodo(tasks =>
tasks.map((task, idx) =>
idx === taskId ? { task: "Edited Todo 1" } : { ...task }
)
);
};
Deleting an object from an array
const deleteTask = (e, taskId = 0) => {
setTodo(tasks => tasks.filter((task, idx) => idx !== taskId));
};
Find the simple working example here.
https://codesandbox.io/s/unruffled-elgamal-h7fhg?file=/src/App.js:482-593
First, Learn basic ES6 before start working on the project.
You can use the previous value returned from the setState to update the existing Array
import { useState } from 'react';
export default function Test() {
const [state, setstate] = useState([1,2,3,4);
return <div onClick={() => setstate((prev) => [...prev,99])}>Heyy{state}</div>;
}
I am working with React and I can't seem to define properly the state of a component (handlePoints). When I want to assign a new state using shallow copy it creates a new entry (key).
Apologies that part of the question is not in code. I couldn't edit it here. If you need more information please reach out. Thanks! I've added an image where you can see that there is a new entry added instead of a modification happening to an existing entry.
cons anecdotes = ['Anecdote A', 'Anecdote B', 'Anecdote C', 'Anecdote D', 'Anecdote E', 'Anecdote F']
const Button = ({text, onClick}) => <div>
<button onClick={onClick}>{text}</button>
const records = () => {
const temp = {};
for (let i = 0; i < anecdotes.length; i++) {
temp[i] = 0;
}
return temp}
const copy = {...records()};
const App = (props) => {
console.log('Copy', copy);
const [selected, setSelected] = useState(0);
const [points, setPoints] = useState(copy);
const handleSelection = () => {
setSelected((Math.random() * (anecdotes.length) ) << 0);
};
const handlePoints = () => {
setPoints({...copy, selected: copy[selected] += 1})
};
return (
<div>
<p>{JSON.stringify({copy})}</p>
<p>{JSON.stringify({points})}</p>
<p>Selected: {selected}</p>
<Button onClick={handleSelection} text={'next anecdote'}/>
<Button onClick={handlePoints} text={'vote'}/>
has {copy[selected]} votes <br/>
{props.anecdotes[selected]} <br/>
</div>
);};
The line where I cannot set the state correctly is:
const handlePoints = () => {
setPoints({...copy, selected: copy[selected] += 1})
};
As you can see, a new entry was added instead of updating one of the existing one.
In terms of why a new field with key 'selected' is being added, is due to how you are creating the new object.
If you want to to update the key with the variable selected, you would need to modify the handlePoints function to be:
const handlePoints = () => {
setPoints({...copy, [selected]: copy[selected] += 1})
};
The brackets around the variable selected is how it determines to use the value of the variable selected. Instead of interpreting it as a the key 'selected'. This is making use of the computed property names introduced in emca2015. More info - Computed property names
Hopefully this solves your problem :)
Good luck :D
Also:
As for what I can see, your copy function it isn't actually copying any object, it seems to be a default? Or is the purpose just to create the keys for the number of anecdotes there are?
Just curious XD
Considering the following declaration:
const [stateObject, setObjectState] = useState({
firstKey: '',
secondKey: '',
});
Are the following snippets both corrects ?
A)
setObjectState((prevState) => ({
...prevState,
secondKey: 'value',
}));
B)
setObjectState({
...stateObject,
secondKey: 'value',
}));
I am sure that A) is correct, but is it necessary ? B) seems ok, but as setObjectState is an asynchronous function, stateObject might not have the most recent value.
One useful thing about case of A that I have found is that you can use this method to update state from child components while only passing down a single prop for setObjectState. For example, say you have parent component with state you would like to update from the child component.
Parent Component:
import React, {useState} from 'react';
import ChildComponent from './ChildComponent';
export const ParentComponent = () => {
const [parentState, setParentState] = useState({
otherValue: null,
pressed: false,
});
return (
<ChildComponent setParentState={setParentState} />
)
}
Child Component:
import React from 'react';
export const ChildComponent = (props) => {
const callback = () => {
props.setParentState((prevState) => ({
...prevState
pressed: true,
}))
}
return (
<button onClick={callback}>test button<button>
)
}
When the button is pressed, you should expect to see that the state has been updated while also keeping its initial values. As for the difference between the two, there isn't much as they both accomplish the same thing.
A will always give you the updated value. B could be correct but might not. Let me give an example:
const Example = props => {
const [counter, setCounter] = useState(0);
useEffect(() => {
// 0 + 1
// In this first case the passed value would be the same as using the callback.
// This is because in this cycle nothing has updated counter before this point.
setCounter(counter + 1);
// 1 + 1
// Thanks to the callback we can get the current value
// which after the previous iexample is 1.
setCounter(latest_value => latest_value + 1);
// 0 + 1
// In this case the value will be undesired as it is using the initial
// counter value which was 0.
setCounter(counter + 1);
}, []);
return null;
};
When the new value depends on the updated one use the callback, otherwise you can simply pass the new value.
const Example = props => {
const [hero, setHero] = useState('Spiderman');
useEffect(() => {
// Fine to set the value directly as
// the new value does not depend on the previous one.
setHero('Batman');
// Using the callback here is not necessary.
setHero(previous_hero => 'Superman');
}, []);
return null;
};
Also in the example you are giving it would probably be better to use two different states:
const [firstKey, setFirstKey] = useState("");
const [secondKey, setSecondKey] = useState("");
I created useBanner hooks
const useBanner = (array, yardage) => {
const [bannArr, setBannArr] = useState(array.slice(0, yardage));
const [bannListIndex, setBannIndex] = useState(1);
return {
....
};
};
Am I doing the right thing and the props throw in useState.
It’s permissible to use useBanner.
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr } = useBanner(array, yardage);
return (
...
);
};
when props will change here.
Will change the state in useBanner.
or is it considered anti-patterns I have to write all this in useMemo
const useBanner = (array, yardage) => {
const [bannArr, setBannArr] = useState([]);
const [bannListIndex, setBannIndex] = useState(1);
useMemo(() => {
setBannArr(array.slice(0, yardage));
setBannIndex(1);
}, [array, yardage]);
return {
....
};
};
Yes, custom hooks are possible in React. Here is separate document discussing custom hooks.
But exactly you sample may require additional code depending on what is your final goal.
If you want initialize state only once, when component Banner is first created, you can just do as in your first sample
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr } = useBanner(array, yardage);
return (
...
);
};
This will work perfectly. But if props array and yardage will change, this will not be reflected in component. So props will be used only once as initial values and then will not be used in useBanner even if changed (And it doesn't matter whether you'll use useBanner or useState directly). This answer highlight this.
If you want to update inital values on each props change, you can go with useEffect like below
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr, setBannArr } = useBanner(array, yardage);
useEffect (() => {
// setBannArr should also be returned from useBanner. Or bannArr should be changed with any other suitable function returned from useBanner.
setBannArr(array.slice(0, yardage));
}, [array, yardage, setBannArr])
return (
...
);
};
In this case Banner component can control state itself and when parent component change props, state in Banner component will be reset to new props.
Here is small sample to showcase second option.