Simply put: Components are not the right place to define these tasks. Currently working towards bringing ember-concurrency
-like modifiers and derived state to redux-saga
. This was a fun exercise.
☎️ React hooks for writing async tasks/workers (with cancellation and concurrency)
This library was inspired by ember-concurrency and uses posterus to run generators as cancellable async-tasks.
Redux-Saga is a fantastic tool for managing these types of tasks. I use it with redux-saga-thunk to get "derived state". It's extremely powerful. But, let's be honest - sagas are a lot of boilerplate. Sometimes, a less-powerful tool can be useful for implementing common patterns.
This library is not intended to replace redux-saga. It is component-lifecycle centric, and hopes to provide some ergonomic tools for consise async tasks:
- Derived state: Never manage boolean flags. Declaratively handle loading, error, and success states.
- Task Modes: By default, tasks can be performed multiple-times in parallel. "Modes" can be selected to modify task behavior. Using a telephone analogy:
- Restartable: "Hang up on current caller when a new call comes in"
- Drop: "If already on the phone, ignore incoming calls"
- Enqueue: "Place callers on hold and handle them in order"
- Keep Latest: ?analogy? Like drop, but the last ignored gets handled when free
- Concurrency: Easily specify concurrency limits
- Cancellation: Cancel individual task instances, or all running instances at once. Component unmounted? Everything cleans up automatically.
$ npm install --save react-use-task
import { useTask } from 'react-use-task';
const BasicTask = () => {
const [{ isRunning, last }, perform] = useTask(function*() {
// Hit some remote api
yield delay(1000);
});
return (
<div>
{/* When task isRunning, button disabled & inner text changed */}
<button disabled={isRunning} onClick={perform}>
{isRunning ? 'Saving...' : 'Perform Task'}
</button>
{last && last.isSuccessful && <div>Saved!</div>}
</div>
);
};
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
import { useWorker } from "react-use-task";
const StockTicker = () => {
const [price, setPrice] = useState();
useWorker(function*() {
// Infinite loop worker
while (true) {
yield delay(1000);
setPrice(Math.random() * 50 + 50);
}
});
if (typeof price === "undefined") {
return <div>Loading...</div>;
}
return <div>Latest stock price: ${price.toFixed(2)}</div>;
};
// By mounting / unmounting the <StockTicker>, the worker loop is
// automatically started & cancelled with the component
const App = () => {
const [showTicker, setShow] = useState(false);
return (
<div>
<button onClick={() => setShow(!showTicker)}>Toggle stock ticker</button>
{showTicker && <StockTicker />}
</div>
);
};
- Non-immediate running of task (e.g. on button press)
-
.drop
,.restartable
,.maxConcurrency
task modifiers (likeember-concurrency
) - Exposing manual cancellation
- Historical state -
last
,lastSuccessful
(likee-c
) - Code sandbox examples
- Finalize
TaskInstance
API - go all-in on futures? - Improve error handling
- More docs / motivaion / examples
- Fancy visualizations (like ember-concurrency)
- Possibly split out some Posterus.Future utilities