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.
Related
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.
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);
I'm learning react native and I'm programing a simple app to register the time of sleep of each day.
When the button that add the new register is pressed I do this
onPress={() => setUpdateAction(true)}
That changes the value of the updateAction:
const [updateAction, setUpdateAction] = useState(false);
When the value of updateAction is changed this will be executed:
useEffect(() => {
... code that add's the register to an array
setviewInfoAction(true);
setUpdateAction(false);
}, [updateAction]);
And inside I call setviewInfoAction(true); becouse I want to change the value that is showed with the value that was inserted.
const [viewInfoAction, setviewInfoAction] = useState(false);
useEffect(() => {
console.log("CALLED");
var seted = false;
for (var i = 0; i < registSleep.length; i++) {
if (
registSleep[i].day === selectedDay &&
registSleep[i].month === selectedMonth &&
registSleep[i].year === selectedYear
) {
setSelectedDayHours(registSleep[i].hours);
seted = true;
}
}
if (!seted) {
setSelectedDayHours(0);
}
setviewInfoAction(false);
}, [viewInfoAction]);
Doing this I was expecting for the second UseEffect to executed but it's not...
The way you have your useEffect set up it will only ever re-run if selectedDay changes. If you want to make it run when setInfoViewAction is executed add viewInfoAction into the dependencies.
Even better because all of this is related logic to the same sideEffect. I would simplify your code by removing the second useEffect and adding the code inside it into the first useEffect. This is mainly just because you can keep all related side effect logic together.
In my functional component I'm storing state with useState hook. Every time my user gets to the bottom of the page, I want to add content. So I added an EventListener on 'scroll' inside a useEffect hook.
The thing is it gets triggered the first time I reach the bottom, my new content appears and my page length increase so I can scroll further. But then, nothing append.
With console.log I checked if my event was well triggered and it is !
It seems like the callback function given to the event listener is stuck in the past and my setter returns the same state as the first time !
The gameTeasersToShow function has 12 elements, I know that if it worked It would crash if I scrolled down a certain good amount of time because the array would not contain enough elems. It's a test.
function Index ({ gameTeasersToShow }) {
console.log(useScrollToBottomDetec())
const [state, setState] = React.useState([gameTeasersToShow[0], gameTeasersToShow[1], gameTeasersToShow[2]])
function handleScrollEvent (event) {
if (window.innerHeight + window.scrollY >= (document.getElementById('__next').offsetHeight)) {
setState([...state, gameTeasersToShow[state.length]])
}
}
React.useEffect(() => {
window.addEventListener('scroll', handleScrollEvent)
return () => {
window.removeEventListener('scroll', handleScrollEvent)
}
}, [])
return (
<>
{
state.map(item => {
const { title, data } = item
return (
<GameTeasers key={title} title={title} data={data} />
)
})
}
</>
)
}
Can you try that?
function handleScrollEvent (event) {
if (window.innerHeight + window.scrollY >= (document.getElementById('__next').offsetHeight)) {
setState(oldState => [...oldState, gameTeasersToShow[oldState.length]])
}
}
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>
);