Why pre-increment does not work with setState - javascript

I have the following functional component
const App = () => {
const [count, setCount] = useState(0);
const test = () => {
setCount(++count);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={test}>Click me</button>
</div>
);
};
when i click on the Click me button, it does not work - notice that i am using pre-increment here
++count
but when i do the same thing with increment only
setCount(count++)
then it works.
Why it is not working with pre-increment ?

Pre or Post increment, either way is the wrong way to increment a counter in React as both mutate state. State is also declared const, so it can't be updated anyway.
const count = 0;
++count; // error
console.log(count);
Use a functional state update.
setCount(c => c + 1);

Related

How do i make my counter value remain, when i refresh the page with localStorage in react

i have tried all i could, i could get a solution, i need my count value to remain, even if i refresh my page. for example if my count is up to 3, and when i refresh my browser, the 3 should remain and not start from zero.
can i get any help?? i am currently learning localstorage
here is the code
import React, { useState } from "react";
export default function LocalStorageSetting() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((prevCount) => prevCount + 1);
};
localStorage.setItem("count", count);
return (
<div>
<button onClick={handleClick}>Increase</button>
<h1>{count}</h1>
</div>
);
}
Read it when you set the initial value
const [count, setCount] = useState(+(localStorage.count || 0));
You can start your state using the value stored in Local Storage, this way, the value count will init by LS value. Keep in mind that Local Storage saves your value as string, so you need to parse the value before init to be sure that your value will be typed as Number
const [count, setCount] = useState(Number(localStorage.count || 0));
Something I like to do it keep the value save in the save function that changed the value, keeping the value changes in a single function.
const handleClick = () => {
setCount((prevCount) => prevCount + 1);
localStorage.setItem("count", count);
};
#epascarello's answer works. You could also use the setCount function you created.
const storageCount = Number(localStorage.getItem("count")) ?? 0;
const [count, setCount] = useState(storageCount);
Also note that localStorage stores string key/value pairs. count should have something like .toString() on the end when adding it as the value. So use localStorage.setItem("count", count.toString()).

useCallback Implementation

I was just wondering how come useCallback cannot pick up the latest state value from the component. Isn't component's state is present in the outer scope.
const [x, updateX] = useState(2);
const handleChange = useCallback(() => {
console.log(x);
// To pick up latest value of x in here, I need to add x in dependency array?
// Why is that this inner arrow function cannot pick x from its outer scope?
}, [])
Edit: The useRef latest value is picked up by the handleChange ..without needing the ref in the dependency array. However we need state value in dependencyArray. Why is that?
I guess there has to be some local scope in between getting created under the hood which is where the value of x is picked up from ? Not sure if I am correct.
Also, follow up question is how to write something like useCallback (function memoization)? using vanilla js ?
Yes, you need to pass X in the dependency array. The callback only gets changed when the dependency changes. In this example you can count up and log the current state of x.
function Tester(props: TesterProps): JSX.Element {
const [x, setX] = useState(0);
const handleChange = useCallback(() => {
console.log(x);
}, [x]);
return (
<>
<button onClick={() => setX(x + 1)}>Change State</button>
<button onClick={() => handleChange()}>Handle Change</button>
</>
);
}
Either do this:
const handleChange = useCallback(() => {
console.log(x);
}, [x]);
Or this:
const handleChange = () => {
console.log(x);
};
Both will print actual X value. The second one doesnt memoize the function, so it will be redeclared each render.

Can I use useRef Hook can also be used to keep track of previous state values

Already use useState, and useEffect to keep track of the previous state. Can I use useRef Hook can also be used to keep track of previous state values.
I belive you can use the useRef to keep track of pervious state value
Check the code example below You can refer this artical https://blog.logrocket.com/accessing-previous-props-state-react-hooks/
function Counter() {
const [count, setCount] = useState(0);
//the useRef Hook allows you to persist data between renders
const prevCountRef = useRef();
useEffect(() => {
//assign the ref's current value to the count Hook
prevCountRef.current = count;
}, [count]); //run this code when the value of count changes
return (
<h1>
Now: {count}, before: {prevCountRef.current}
{/*Increment */}
<button onClick={() => setCount((count) => count + 1)}>Increment</button>
</h1>
);
}

Strange behaviour when removing element from array of divs (React Hooks)

I am struggling to understand a strange behaviour while deleting an element from an array of divs.
What I want to do is create an array of divs representing a list of purchases. Each purchase has a delete button that must delete only the clicked one. What is happening is that when the delete button is clicked on the purchase x all the elements with indexes greather than x are deleted.
Any help will be appreciated, including syntax advices :)
import React, { useState } from "react";
const InvestmentSimulator = () => {
const [counter, increment] = useState(0);
const [purchases, setPurchases] = useState([
<div key={`purchase${counter}`}>Item 0</div>
]);
function addNewPurchase() {
increment(counter + 1);
const uniqueId = `purchase${counter}`;
const newPurchases = [
...purchases,
<div key={uniqueId}>
<button onClick={() => removePurchase(uniqueId)}>delete</button>
Item number {uniqueId}
</div>
];
setPurchases(newPurchases);
}
const removePurchase = id => {
setPurchases(
purchases.filter(function(purchase) {
return purchase.key !== `purchase${id}`;
})
);
};
const purchasesList = (
<div>
{purchases.map(purchase => {
if (purchases.indexOf(purchase) === purchases.length - 1) {
return (
<div key={purchases.indexOf(purchase)}>
{purchase}
<button onClick={() => addNewPurchase()}>add</button>
</div>
);
}
return purchase;
})}
</div>
);
return <div>{purchasesList}</div>;
};
export default InvestmentSimulator;
There are several issues with your code, so I'll go through them one at a time:
Don't store JSX in state
State is for storing serializable data, not UI. You can store numbers, booleans, strings, arrays, objects, etc... but don't store components.
Keep your JSX simple
The JSX you are returning is a bit convoluted. You are mapping through purchases, but then also returning an add button if it is the last purchase. The add button is not related to mapping the purchases, so define it separately:
return (
<div>
// Map purchases
{purchases.map(purchase => (
// The JSX for purchases is defined here, not in state
<div key={purchase.id}>
<button onClick={() => removePurchase(purchase.id)}>
delete
</button>
Item number {purchase.id}
</div>
))}
// An add button at the end of the list of purchases
<button onClick={() => addNewPurchase()}>add</button>
</div>
)
Since we should not be storing JSX in state, the return statement is where we turn our state values into JSX.
Don't give confusing names to setter functions.
You have created a state variable counter, and named the setter function increment. This is misleading - the function increment does not increment the counter, it sets the counter. If I call increment(0), the count is not incremented, it is set to 0.
Be consistent with naming setter functions. It is the accepted best practice in the React community that the setter function has the same name as the variable it sets, prefixed with the word "set". In other words, your state value is counter, so your setter function should be called setCounter. That is accurate and descriptive of what the function does:
const [counter, setCounter] = useState(0)
State is updated asynchronously - don't treat it synchronously
In the addNewPurchase function, you have:
increment(counter + 1)
const uniqueId = `purchase${counter}`
This will not work the way you expect it to. For example:
const [myVal, setMyVal] = useState(0)
const updateMyVal = () => {
console.log(myVal)
setMyVal(1)
console.log(myVal)
}
Consider the above example. The first console.log(myVal) would log 0 to the console. What do you expect the second console.log(myVal) to log? You might expect 1, but it actually logs 0 also.
State does not update until the function finishes execution and the component re-renders, so the value of myVal will never change part way through a function. It remains the same for the whole function.
In your case, you're creating an ID with the old value of counter.
The component
Here is an updated version of your component:
const InvestmentSimulator = () => {
// Use sensible setter function naming
const [counter, setCounter] = useState(0)
// Don't store JSX in state
const [purchases, setPurchases] = useState([])
const addNewPurchase = () => {
setCounter(prev => prev + 1)
setPurchases(prev => [...prev, { id: `purchase${counter + 1}` }])
}
const removePurchase = id => {
setPurchases(prev => prev.filter(p => p.id !== id))
}
// Keep your JSX simple
return (
<div>
{purchases.map(purchase => (
<div key={purchase.id}>
<button onClick={() => removePurchase(purchase.id)}>
delete
</button>
Item number {purchase.id}
</div>
))}
<button onClick={() => addNewPurchase()}>add</button>
</div>
)
}
Final thoughts
Even with these changes, the component still requires a bit of a re-design.
For example, it is not good practice to use a counter to create unique IDs. If the counter is reset, items will share the same ID. I expect that each of these items will eventually store more data than just an ID, so give them each a unique ID that is related to the item, not related to its place in a list.
Never use indices of an array as key. Check out this article for more information about that. If you want to use index doing something as I did below.
const purchasesList = (
<div>
{purchases.map((purchase, i) => {
const idx = i;
if (purchases.indexOf(purchase) === purchases.length - 1) {
return (
<div key={idx}>
{purchase}
<button onClick={() => addNewPurchase()}>add</button>
</div>
);
}
return purchase;
})}
</div>
);

React useEffect stale value inside function

How would one update the value of variable simulationOn inside of function executeSimulation in the following context:
App this.state.simulationOn changes via external code --> ... --> React stateless component (Robot) rerendered --> useEffect hook called with new values --> executeSimulation IS NOT UPDATED with new value of simulationOn.
function Robot({ simulationOn, alreadyActivated, robotCommands }) {
useEffect(() => {
function executeSimulation(index, givenCommmands) {
index += 1;
if (index > givenCommmands.length || !simulationOn) {
return;
}
setTimeout(executeSimulation.bind({}, index, givenCommmands), 1050);
}
if (simulationOn && !alreadyActivated) {
executeSimulation(1, robotCommands);
}
}, [simulationOn, alreadyActivated, robotCommands]);
}
In the example above, simulationOn never changes to false, even though useEffect is called with the updated value (I check with console.log). I suspect this is because the new value of simulationOn is never passed to the scope of function executeSimulation, but I don't know how to pass new hook values inside of function executeSimulation.
The executeSimulation function has a stale closure simulationOn will never be true, here is code demonstrating stale closure:
var component = test => {
console.log('called Component with',test);
setTimeout(
() => console.log('test in callback:', test),
20
);
}
component(true);
coponent(false)
Note that Robot is called every time it renders but executeSimulation runs from a previous render having it's previous simulationOn value in it's closure (see stale closure example above)
Instead of checking simulationOn in executeSimulation you should just start executeSimulation when simulationOn is true and clearTimeout in the cleanup function of the useEffect:
const Component = ({ simulation, steps, reset }) => {
const [current, setCurrent] = React.useState(0);
const continueRunning =
current < steps.length - 1 && simulation;
//if reset or steps changes then set current index to 0
React.useEffect(() => setCurrent(0), [reset, steps]);
React.useEffect(() => {
let timer;
function executeSimulation() {
setCurrent(current => current + 1);
//set timer for the cleanup to cancel it when simulation changes
timer = setTimeout(executeSimulation, 1200);
}
if (continueRunning) {
timer = setTimeout(executeSimulation, 1200);
}
return () => {
clearTimeout(timer);
};
}, [continueRunning]);
return (
<React.Fragment>
<h1>Step: {steps[current]}</h1>
<h1>Simulation: {simulation ? 'on' : 'off'}</h1>
<h1>Current index: {current}</h1>
</React.Fragment>
);
};
const App = () => {
const randomArray = (length = 3, min = 1, max = 100) =>
[...new Array(length)].map(
() => Math.floor(Math.random() * (max - min)) + min
);
const [simulation, setSimulation] = React.useState(false);
const [reset, setReset] = React.useState({});
const [steps, setSteps] = React.useState(randomArray());
return (
<div>
<button onClick={() => setSimulation(s => !s)}>
{simulation ? 'Pause' : 'Start'} simulation
</button>
<button onClick={() => setReset({})}>reset</button>
<button onClick={() => setSteps(randomArray())}>
new steps
</button>
<Component
simulation={simulation}
reset={reset}
steps={steps}
/>
<div>Steps: {JSON.stringify(steps)}</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
simulationOn is never changed, because the parent component has to change it. It is passed in a property to this Robot component.
I created a sandbox example to show, if you change it properly in the parent, it will propagate down.
https://codesandbox.io/s/robot-i85lf.
There a few design issues in this robot. It seems assume a Robot can "remember" the index value by holding it as a instance variable. React doesn't work that way. Also, it assume that useEffect will be called exactly once when one parameter is changed, which is not true. We don't know how many times useEffect will be invoked. React only guarantee that it will be called if one of the dependencies are changed. But it could be invoked more often.
My example shows that a command list has to be kept by the parent, and the full list needs to be sent, so the dumb Robot can show all the commands it executed.

Categories

Resources