useCallback Implementation - javascript

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.

Related

Why pre-increment does not work with setState

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);

What is the scoping logic behind React states being used inside functions in custom hooks?

So say you have a custom hook:
useCustomHook()=>{
const [state, setState] = React.useState(0);
const modifyState = ({state, n}) => {setState(state + n);}
/*Does state need to be included as an argument/parameter here in modifyState? If, alternatively,
const modifyState = ({n}) => {setState(state + n)};
Will state always be 0 in the scope of modifyState, since that was its value when the function was
created originally. So everytime modifyState is called, it is equivalent to (n)=>setState(0+n) ?
*/
return [state, modifyState];
}
const FunctionalComponent = () => {
const [state, modifyState] = useCustomHook();
const n = 5;
modifyState({state,n}) /* Does state need to be passed here? (since useCustomHook already has its own
copy of state) */
//... logic ....
return <div></div>
}
From doing some testing in the console, it appears that state doesn't need to be passed as an argument to modifyState. But, I'm confused as to the scoping logic behind this, and am unsure if the behavior hooks would change it. Could someone explain the logic behind this?
Instead of passing the state you can make use of function implementation of setState. Something like this:
const modifyState = (n) => {
setState(state => state + n);
}
Ref: https://reactjs.org/docs/hooks-reference.html#functional-updates

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.

React Hooks - using useState vs just variables

React Hooks give us useState option, and I always see Hooks vs Class-State comparisons. But what about Hooks and some regular variables?
For example,
function Foo() {
let a = 0;
a = 1;
return <div>{a}</div>;
}
I didn't use Hooks, and it will give me the same results as:
function Foo() {
const [a, setA] = useState(0);
if (a != 1) setA(1); // to avoid infinite-loop
return <div>{a}</div>;
}
So what is the diffrence? Using Hooks even more complex for that case...So why start using it?
The reason is if you useState it re-renders the view. Variables by themselves only change bits in memory and the state of your app can get out of sync with the view.
Compare this examples:
function Foo() {
const [a, setA] = useState(0);
return <div onClick={() => setA(a + 1)}>{a}</div>;
}
function Foo() {
let a = 0;
return <div onClick={() => a = a + 1}>{a}</div>;
}
In both cases a changes on click but only when using useState the view correctly shows a's current value.
Local variables will get reset every render upon mutation whereas state will update:
function App() {
let a = 0; // reset to 0 on render/re-render
const [b, setB] = useState(0);
return (
<div className="App">
<div>
{a}
<button onClick={() => a++}>local variable a++</button>
</div>
<div>
{b}
<button onClick={() => setB(prevB => prevB + 1)}>
state variable b++
</button>
</div>
</div>
);
}
function Foo() {
const [a, setA] = useState(0);
if (a != 1) setA(1); // to avoid infinite-loop
return <div>{a}</div>;
}
is equivalent to
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {
a: 0
};
}
// ...
}
What useState returns are two things:
new state variable
setter for that variable
if you call setA(1) you would call this.setState({ a: 1 }) and trigger a re-render.
Your first example only works because the data essentially never changes. The enter point of using setState is to rerender your entire component when the state hanges. So if your example required some sort of state change or management you will quickly realize change values will be necessary and to do update the view with the variable value, you will need the state and rerendering.
Updating state will make the component to re-render again, but local values are not.
In your case, you rendered that value in your component.
That means, when the value is changed, the component should be re-rendered to show the updated value.
So it will be better to use useState than normal local value.
function Foo() {
let a = 0;
a = 1; // there will be no re-render.
return <div>{a}</div>;
}
function Foo() {
const [a, setA] = useState(0);
if (a != 1) setA(1); // re-render required
return <div>{a}</div>;
}
It is perfectly acceptable to use standard variables. One thing I don't see mentioned in other answers is that if those variables use state-variables, their value will seemingly update on a re-render event.
Consider:
import {useState} from 'react';
function NameForm() {
// State-managed Variables
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// State-derived Variables
const fullName = `${firstName} ${lastName}`;
return (
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
<input value={lastName} onChange={e => setLastName(e.target.value)} />
{fullName}
);
}
/*
Description:
This component displays three items:
- (2) inputs for _firstName_ and _lastName_
- (1) string of their concatenated values (i.e. _lastName_)
If either input is changed, the string is also changed.
*/
Updating firstName or lastName sets the state and causes a re-render. As part of that process fullName is assigned the new value of firstName or lastName. There is no reason to place fullName in a state variable.
In this case it is considered poor design to have a setFullName state-setter because updating the firstName or lastName would cause a re-render and then updating fullName would cause another re-render with no perceived change of value.
In other cases, where the view is not dependent on the variable, it is encouraged to use local variables; for instance when formatting props values or looping; regardless if whether the value is displayed.

Categories

Resources