Change state while waiting for loop to finish - javascript

OnClick I want to call a function that sets my loading state to true then do a for loop which will take more than one second and on the last loop set the loading state back to false but in my code the loading state doesn't change as expected. What do I have to fix?
import { useState } from "react"
const Test = () => {
const [loading, setLoading] = useState(false)
const someFunction = () => {
setLoading(true)
const someVeryBigArray = [...]
for (let i = 0; i < someVeryBigArray.length; i++) {
if (i === someVeryBigArray.length - 1) {
setLoading(false)
}
}
}
return (
<button onClick={someFunction} className={`${loading && "text-red-500"}`}>
test
</button>
)
}
export default Test

You need to give the browser time to re-render. If you have a huge blocking loop, React won't be yielding control back to the browser so that it can repaint (or even to itself so that the component can run again with the new state).
While one approach would be to run the expensive function in an effect hook, after the new loading state has been rendered:
const Test = () => {
const [running, setRunning] = useState(false)
useEffect(() => {
if (!running) return;
const someVeryBigArray = [...]
for (let i = 0; i < someVeryBigArray.length; i++) {
// ...
}
setRunning(false);
}, [running]);
return (
<button onClick={() => setRunning(true)} className={running && "text-red-500"}>
test
</button>
)
}
A better approach would be to offload the expensive code to either the server, or to a web worker that runs on a separate thread, so as not to interfere with the UI view that React's presenting.

To be honest if in any case your loop is taking 1 second to run, then this will cost into you app's performance. And this is not the best way to do as well.
The better way would be, If your really want to replicate the delay in you app then you should use setTimeout() using which you delay some action. sharing a code snippet might help you.
JSX
import { useEffect, useState } from "react";
const Test = () => {
const [loading, setLoading] = useState(false);
let timevar = null;
const someFunction = () => {
setLoading(true);
timevar = setTimeout(() => {
setLoading(false); //this will run after 1 second
}, 1000); //1000 ms = 1 second
};
useEffect(() => {
return () => {
//clear time out if in case component demounts during the 1 second
clearTimeout(timevar);
};
});
return (
<button onClick={someFunction} className={`${loading && "text-red-500"}`}>
test
</button>
);
};
export default Test;

Related

Is it possible to create a promise that get fulfilled by a state change in react-native?

I have a group of react-native components that have similar interfaces. The important method in the interface is the run method. Users can execute this run method by pressing an action button for each of the components or by pressing an action button in the parent level, that will iterate through each of these similar components using their ref and call the run method. Here is a simplified version of the setup.
const Test1 = forwardRef((props, ref) => {
const [importantState, setimportantState] = useState('initial')
useImperativeHandle(ref, () => ({
run: run,
}))
const run = async () => {
// This function will open a modal
// where users will eventually change the important state
openInteractionModal()
// The idea of this promiss is to settle when the users
// eventually change the importantState thorugh thier interaction
// It does so by checking the state every 100 miliseconds
// This however is a problem as a result of the setInterval function will
// inclose the initial state instead of getting the new value each time
return new Promise((resolve, reject) => {
let interval = setInterval(() => {
if (importantStat === 'complete') {
resolve('success')
clearInterval(interval)
} else if (importantStat === 'failed') {
reject('failed')
clearInterval(interval)
}
}, 100)
})
}
return (
<>
<Button title="run" onPress={run} />
</>
)
})
const parentComponent = () => {
const test1Ref = userRef(null)
const test2Ref = userRef(null)
const test3Ref = userRef(null)
const testRefs = [test1Ref, test2Ref, test3Ref]
const autoRun = async () => {
testRefs.forEach(ref => {
await ref?.current.run()
});
}
return (
<>
<Button title="Auto run" onPress={autoRun} />
<Test1 ref={test1Ref} />,
<Test2 ref={test2Ref} />,
<Test3 ref={test3Ref} />,
</>
)
}
The problem I am having right now is the promise I am returning from these individual components never settles as a result of the closure I mentioned above in the comments. Is there a way to overcome this closure, or is there a better react-native pattern that I am using right now?
Don't go polling. Instead, pass the resolve function as an argument to openInteractionModal, and call it together with setImportantState from the modal dialog.
If that is for some reason not possible, you can use a ref to store the resolve function and run it from an effect when the state changes:
const onImportantStateChange = useRef();
useEffect(() => {
if (!onImportantStateChange.current) return;
if (importantState === 'complete') {
onImportantStateChange.current('success');
onImportantStateChange.current = undefined;
} else if (importantState === 'failed') {
onImportantStateChange.current(Promise.reject(new Error('failure')));
onImportantStateChange.current = undefined;
}
}, [importantState])
const run = async () => {
return new Promise(resolve => {
onImportantStateChange.current = resolve;
// This function will open a modal
// where users will eventually change the important state
openInteractionModal();
});
}
Create a ref and store the status in that instead of the state of the Test1 component.
Like that
const importantState = useRef(null);
Set value in the current of the ref
importantState.current="complete"
Later use it in the setInterval like
if(importantState.current === "complete")

React hooks: get current value from child component with no constant rerender

There is counter on page. To avoid re-rendering the entire Parent component every second, the counter is placed in a separate Child component.
From time to time there is need to take current time from counter Child (in this example by clicking Button).
I found solution with execution useEffect by passing empty object as dependency.
Even though it works, I don't feel this solution is correct one.
Do you have suggestion how this code could be improved?
Parent component:
const Parent = () => {
const [getChildValue, setGetChildValue] = useState(0);
const [triggerChild, setTriggerChild] = useState(0); // set just to force triggering in Child
const fooJustToTriggerChildAction = () => {
setTriggerChild({}); // set new empty object to force useEffect in child
};
const handleValueFromChild = (timeFromChild) => {
console.log('Current time from child:', timeFromChild);
};
return (
<>
<Child
handleValueFromChild={handleValueFromChild}
triggerChild={triggerChild}
/>
<Button onPress={fooJustToTriggerChildAction} >
Click to take time
</Button>
</>
);
};
Child component
const Child = ({
triggerChild,
handleValueFromChild,
}) => {
const [totalTime, setTotalTime] = useState(0);
const totalTimeRef = useRef(totalTime); // useRef to handle totalTime inside useEffect
const counter = () => {
totalTimeRef.current = totalTimeRef.current + 1;
setTotalTime(totalTimeRef.current);
setTimeout(counter, 1000);
};
useEffect(() => {
counter();
}, []); // Run time counter at first render
useEffect(() => {
const valueForParent = totalTimeRef.current;
handleValueFromChild(valueForParent); // use Parent's function to pass new time
}, [triggerChild]); // Force triggering with empty object
return (
<>
<div>Total time: {totalTime}</div>
</>
);
};
Given your set of requirements, I would do something similar, albeit with one small change.
Instead of passing an empty object (which obviously works, as {} !== {}), I would pass a boolean flag to my child, requesting to pass back the current timer value. As soon as the value is passed, I would then reset the flag to false, pass the value and wait for the next request by the parent.
Parent component:
const Parent = () => {
const [timerNeeded, setTimerNeeded] = useState(false);
const fooJustToTriggerChildAction = () => {
setTimerNeeded(true);
};
const handleValueFromChild = (timeFromChild) => {
console.log('Current time from child:', timeFromChild);
setTimerNeeded(false);
};
return (
<>
<Child
handleValueFromChild={handleValueFromChild}
timerNeeded={timerNeeded}
/>
<Button onPress={fooJustToTriggerChildAction} >
Click to take time
</Button>
</>
);
};
Child component
const Child = ({
timerNeeded,
handleValueFromChild,
}) => {
const [totalTime, setTotalTime] = useState(0);
const totalTimeRef = useRef(totalTime); // useRef to handle totalTime inside useEffect
const counter = () => {
totalTimeRef.current = totalTimeRef.current + 1;
setTotalTime(totalTimeRef.current);
setTimeout(counter, 1000);
};
useEffect(() => {
counter();
}, []); // Run time counter at first render
useEffect(() => {
if (timerNeeded) {
handleValueFromChild(totalTimeRef.current);
}
}, [timerNeeded]);
return (
<>
<div>Total time: {totalTime}</div>
</>
);
};

How can I stop this recurstion function

import React, { useCallback, useState } from "react";
import { chat_chat } from "./chat_data.json"; // Length of the chat_chat array is over 10000
function App() {
const [slug, setSlug] = useState(0);
// const [stop, setStop] = useState(false);
// const stopFn = useCallback(() => {
// setStop(!stop);
// }, [stop]);
const slugAndURLImport = useCallback(() => {
let i = 0;
let stop = false;
const loopFn = () => {
setTimeout(() => {
console.log("stop", i, stop);
if (chat_chat.length < i || stop) {
return;
}
setSlug(chat_chat[i].slug);
i++;
loopFn();
}, 2000);
};
loopFn();
return () => {
stop = true;
};
}, []);
return (
<>
<div>
<button onClick={slugAndURLImport}> "Start" </button>
</div>
<div>{slug}</div>
<div>
<button
onClick={() => {
slugAndURLImport()();
}}>
Stop
</button>
</div>
</>
);
}
export default App;
Is there a way to stop the loopFn that is running?
loopFn is a function that imports data from arrays in json.
Or is there any other way than recursive function?
I want to stop the function when I press the button.
However, a new function starts and stops, and a function that is in progress does not stop.
Is there no way that javascript can stop the function in progress?
I'd like to use it in a react hooks.
You could use setInterval instead of a setTimeout in a recursive function. If you store the output of setInterval to a variable (maybe use a useRef hook) then you can call clearInterval to stop the loop.

setInterval using Next.js and hooks

I'm trying to implement a loader using Next.js and hooks.
I have to do a fetch to check new data every 3 minutes and when that happens, I want to use a loading state.
I created a setLoading that starts at false and inside a setInterval pass to true, but I don't 'know why doesn't work.
Here is my code:
import React, { useState, useEffect } from "react";
import Context from "../../config/Context";
import { checkNewData } from "../../helper/index";
function Home() {
const [contentLoading, setLoading] = useState(false);
const data = context.events[0];
useEffect(() => {
setInterval(() => {
if (data.live == false) {
setLoading(true);
checkNewData();
}
setLoading(false)
}, 10000);
return (
<React.Fragment>
{contentLoading ? <p>loading</p> : <p>no loading</p>
</React.Fragment>
);
}
export default Home;
Ok, so here You have working interval
(Every 3 sec calling api, and counts how many times it works(not needed just FYI)
In the function responsible for Api call you should turn on the loader - then every 3 seconds the data is called again
const Home = (props) => {
const [loading, setLoading] = useState(false);
const [check, setCheck] = useState(0)
const callApi = () => {
Here you should call your api + set loader
if fetching(setLoading(true))
if fetched(setLoading(false))
}
useEffect(() => {
const id = setInterval(() => {
callApi()
setCheck(check + 1)
}, 3000);
return () => clearInterval(id);
}, [check])
return (
<React.Fragment>
{loading ? <p>Loading</p> : <p>No Loading</p>}
<p>Times check execute {check}</p>
</React.Fragment>
);
}
A better way to do this now will be to use a plugin like SWR. It will handle the data fetch and even the loading state seamlessly.
Check the documentation here: https://swr.vercel.app

React hooks: How to read & update state in hooks without infinite loops with react-hooks/exhaustive-deps rule

When state is in a hook it can become stale and leak memory:
function App() {
const [greeting, setGreeting] = useState("hello");
const cb = useCallback(() => {
alert("greeting is " + greeting);
}, []);
return (
<div className="App">
<button onClick={() => cb()}>Click me</button>
<p>
Click the button above, and now update the greeting by clicking the one
below:
</p>
<button onClick={() => setGreeting("bye")}>
Update greeting
</button>
<p>Greeting is: {greeting}</p>
<p>
Now click the first button again and see that the callback still has the
old state.
</p>
</div>
);
}
Demo: https://codesandbox.io/s/react-hook-stale-datamem-leak-demo-9pchk
The problem with that is that we will run into infinite loops in a typical scenario to fetch some data if we follow Facebook's advice to list all dependencies always, as well as ensure we don't have stale data or memory leaks (as the example showed above):
const [state, setState] = useState({
number: 0
});
const fetchRandomNumber = useCallback(async () => {
if (state.number !== 5) {
const res = await fetch('randomNumber');
setState(v => ({ ...v, number: res.number }));
}
}, [setState, state.number]);
useEffect(() => {
fetchRandomNumber();
}, [fetchRandomNumber]);
Since Facebook say we should list fetchRandomNumber as a dependency (react-hooks/exhaustive-deps ESLint rule) we have to use useCallback to maintain a reference, but it regenerates on every call since it both depends on state.number and also updates it.
This is a contrived example but I've run into this many times when fetching data. Is there a workaround for this or is Facebook wrong in this situation?
Use the functional form of the state setter:
const fetchData = useCallback(async () => {
const res = await fetch(`url?page=${page}`);
setData((data) => ([...data, ...res.data]));
setPage((page) => page + 1);
}, [setData, setPage]);
Now you don't need data and page as your deps
You can also use a ref to run the effect only on mount :
const mounted = useRef(false);
useEffect(() => {
if(!mounted.current) {
fetchSomething();
mounted.current = true;
}
return () => { mounted.current = false }
}, [fetchSomething]);
And
const fetchSomething = useCallback(async () => {
...
}, [setData, setPage, data, page]);
fetchSomething is not a dependency here. You don't want to retrigger the effect, you only cause it once when the component mounts. Thats what useEffect(() => ..., []) is for.

Categories