In a React app, I would like to have a function initiate a series of async calls and then have the ability to alter the state available to those calls as they run. As an example, I would let the user initiate a retrieval of 5 data files, which could run in the background and take minutes, but give them the option to abort the process or trim the total file count.
Here's an idea of what it could look like, but unfortunately this pattern doesn't seem to work:
function App() {
const [halt, setHalt] = useState(false);
return (
...
<button onClick={() => longProcess(halt)}>Start</button>
<button onClick={() => setHalt(true)}>Stop</button>
...
);
}
async function longProcess(halt) {
for (const fileid of files_to_get) {
// For example, halt if the user clicks the Stop button during execution
if (halt) break;
await getDataFile(fileid);
}
}
Ideally, I want to use pure functional ponents and to allow the async function to be available for use by multiple ponents. So I've been using React Hooks across the board. I have e up with 3 solutions, but none of them quite fit the bill:
- Using a class ponent,
this.state
will update asynchronously- Example
- Downsides: not a functional ponent, async function is tied to the ponent
useRef()
is a suggested option- Example
- Downsides: we no longer get re-renders, and is this a mon useRef pattern?
- Pass the setter from useState, pass a function when calling it which will retrieve the current value
- Example
- Downsides: seems VERY hacky :)
I'd be curious if there's any clean way similar to the 3rd example, that I just haven't e across in my limited React experience. Other suggestions wele as well!
In a React app, I would like to have a function initiate a series of async calls and then have the ability to alter the state available to those calls as they run. As an example, I would let the user initiate a retrieval of 5 data files, which could run in the background and take minutes, but give them the option to abort the process or trim the total file count.
Here's an idea of what it could look like, but unfortunately this pattern doesn't seem to work:
function App() {
const [halt, setHalt] = useState(false);
return (
...
<button onClick={() => longProcess(halt)}>Start</button>
<button onClick={() => setHalt(true)}>Stop</button>
...
);
}
async function longProcess(halt) {
for (const fileid of files_to_get) {
// For example, halt if the user clicks the Stop button during execution
if (halt) break;
await getDataFile(fileid);
}
}
Ideally, I want to use pure functional ponents and to allow the async function to be available for use by multiple ponents. So I've been using React Hooks across the board. I have e up with 3 solutions, but none of them quite fit the bill:
- Using a class ponent,
this.state
will update asynchronously- Example
- Downsides: not a functional ponent, async function is tied to the ponent
useRef()
is a suggested option- Example
- Downsides: we no longer get re-renders, and is this a mon useRef pattern?
- Pass the setter from useState, pass a function when calling it which will retrieve the current value
- Example
- Downsides: seems VERY hacky :)
I'd be curious if there's any clean way similar to the 3rd example, that I just haven't e across in my limited React experience. Other suggestions wele as well!
Share Improve this question asked Nov 17, 2019 at 10:36 Derek LarsonDerek Larson 531 silver badge5 bronze badges 4- Why is it a problem to have the async function inside the ponent? – Alvaro Commented Nov 17, 2019 at 10:57
-
approach with
useRef
fits that case to me, why do you think it does not? – skyboyer Commented Nov 17, 2019 at 11:16 - @Alvaro I'd like to be able to reuse this function across several ponents, as it can be pretty fundamental. A decent option is to make a generic function and then include a small wrapper in each ponent, but I'm curious if we can do better! – Derek Larson Commented Nov 17, 2019 at 11:30
- @skyboyer Yeah, it's not bad, and what I'll use for now. I don't like giving up the re-render though. For example, I'd like some kind of UI cue that the halt was recognized. Say we're in the middle of a single, long fetch and the user clicks 'halt', then it might be confusing if nothing happens. If there's an easy way to cause a re-render here, I'm all ears! – Derek Larson Commented Nov 17, 2019 at 11:32
2 Answers
Reset to default 4To have a reusable function I would define it inside a Hook.
The following proposal uses useState
to execute the function. We need useState
to trigger a render when the value changes. This value will call the function from inside a useEffect
.
It also uses useRef
so that the process can start and later read its value, that could have changed during execution.
const App = () => {
const { startProcess, stopProcess } = useLongProcess();
return (
<Fragment>
<button onClick={startProcess}>Start</button>
<button onClick={stopProcess}>Stop</button>
</Fragment>
);
};
const useLongProcess = () => {
const stop = useRef(false);
const [start_process, setStartProcess] = useState(false);
useEffect(() => {
if (!start_process) {
return;
}
const longProcess = async () => {
for (const fileid of files_to_get) {
if (stop.current) break;
await getDataFile(fileid);
}
};
longProcess();
}, [start_process]);
return {
startProcess: () => setStartProcess(true),
stopProcess: () => {
stop.current = true;
}
};
};
Right now you have no way of updating the halt
state inside the longProcess
function once it's called. So you could work with some global
object to update the state but that is not best practice.
So maybe take a look at React's useCallback
and other hooks, and maybe change your loop to recursively calling one of those hooks so maybe this can help to update the state. Hope this helps even though I don't know a solution right now.