https://github.com/ecomfe/use-optimistic
React hooks to help manage optimistic states
https://github.com/ecomfe/use-optimistic
Last synced: 11 months ago
JSON representation
React hooks to help manage optimistic states
- Host: GitHub
- URL: https://github.com/ecomfe/use-optimistic
- Owner: ecomfe
- License: mit
- Created: 2019-08-10T07:42:10.000Z (almost 7 years ago)
- Default Branch: master
- Last Pushed: 2022-12-11T01:28:41.000Z (over 3 years ago)
- Last Synced: 2025-07-06T15:49:31.576Z (12 months ago)
- Language: JavaScript
- Size: 3.08 MB
- Stars: 8
- Watchers: 10
- Forks: 1
- Open Issues: 24
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# use-optimistic
This is a set of react hooks to help manage optimistic states.
As previously stated in [redux-optimistic-thunk](https://github.com/ecomfe/redux-optimistic-thunk#why-this-middleware), manually managing optimistic states, commits, rollbacks and transactions are not ideal model of state management. React hooks provides powers to manage states in a more functional way, and this library aimed to build optimistic functions above hooks.
**This library required [ES6 Generators](https://caniuse.com/#feat=es6-generators) to work.**
## Usage
### Install
```shell
npm install use-optimistic
```
This library provides 3 hooks to developers.
### useOptimisticFactory
This is the fundamental hooks which manages a full functional optimistic state:
```js
const [state, dispatch] = useOptimisticFactory(factory, initialState);
```
The `factory` parameter referes to a function receiving a `payload` object and returns either:
- A state reducer `(state: T) => T`, this reducer will be executed immediately providing current state, the returned state is going to be the next state.
- A tuple of `[asyncWorkflow, optimisticReducer]` which defines an async workflow and a optimistic reducer to take place before async operations complete.
An `asyncWorkflow` is a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) which yields a reducer or a `Promise` instance.
Any time a state reducer is yielded, it will be executed against current state and generates the next state.
When a `Promise` is yielded, it will be awaited, the resolved value will be returned to `yield` statement.
Right after the first `Promise` is yielded, the `optimisticReducer` will be executed to generate an optimistic state, inside `useOptimisticFactory` hook it will automatically rollback this optimistic state after `Promise` is settled (either fulfilled or rejected).
Note that `optimisticReducer` will be only executed on the first `Promise`, so if `asyncWorkflow` yields several `Promise`s the later ones will not take benefit from optimistic state.
The return value of `useOptimisticFactory` is the same signature of `useReducer`, the `state` represents the latest state and `dispatch` is a function to feed `payload` to `factory` argument.
One thing to metion is that `dispatch` will be different if `factory` changes, this is different to the built-in `useReducer` hook, we recommend to cache `factory` with `useCallback`.
This is a simple example to manage a todo list with `useOptimisticFactory`:
```js
const factory = useCallback(
({type, payload}) => {
switch (type) {
case 'DELETE':
return items => {
const index = items.findIndex(i => i.id === payload);
return [
...items.slice(0, index),
{...items[index], deleted: true},
...items.slice(index + 1),
];
};
case 'CREATE':
return [
function* create() {
// Await an async api call
const newTodo = yield saveTodo(payload);
// Insert the returned new todo to list, with pending set to false
yield items => [
...items,
{...newTodo, pending: false, deleted: false},
];
},
items => [
...items,
// Insert an optimistic item with property pending set to true,
// this item will be removed after saveTodo resolves
{id: uid(), text: payload, pending: true, deleted: false},
],
];
default:
return s => s;
}
},
[]
);
const [todos, dispatch] = useOptimisticFactory(factory, []);
```
You can call `dispatch` at any time, parallelism is handled internally. See [demo](demo/components/App/index.js) to find more details.
### useOptimisticState
Like `useState` and `useReducer`, `useOptimisticState` is a simmple encapsulation to `useOptimisticFactory`.
```js
const [state, setState] = useOptimisticState(initialState);
```
The `setState` can receive 2 different arguments:
```js
setState(nextState);
setState(promise, optimisticNextState);
```
If only 1 argument is provided, this works exactly the same as `useState` hook, `nextState` can be either a state object or a state reducer `(state: T) => T`.
When 2 arguments are provided, the first one is a `Promise` which resolves to a `nextState` (which is a state object or a reducer), the second is a `nextState` takes optimistic effects.
```js
const [todos, setTodos] = useOptimisticState([]);
const addTodo = todo => setState(
(async () => {
const newTodo = await saveTodo(todo);
// We recommend to use a reducer since it is asynchronous
return todos => [...todos, {...newTodo, pending: false, deleted: false}];
})(),
// Optimistic next state is executed synchronously, it can be a single state object
[...todos, {...todo, pending: true, deleted: false}]
);
```
### useOptimisticTask
This is a binding of `useOptimisticState` and an async task.
```js
const [state, run] = useOptimisticTask(task, optimisticTask);
```
- The `task` is an async function `(arg: TArg) => Promise`.
- The `optimisticTask` is a sync version of task provides an optimistic response `(arg: TArg) => TState`.
- Returned `run` function receives the same argument as `task`.
```js
const newTodo = async todo => {
const newTodo = await saveTodo(todo);
// We recommend to use a reducer since it is asynchronous
return todos => [...todos, {...newTodo, pending: false, deleted: false}];
};
const optimisticNewTodo = todo => todos => [...todos, {...todo, pending: true, deleted: false}];
const [todos, addTodo] = useOptimisticTask(newTodo, optimisticNewTodo, []);
```
`useOptimisticTask` is useful when encapsulating business aware hooks.