{"id":29926649,"url":"https://github.com/ecomfe/redux-optimistic-thunk","last_synced_at":"2025-08-02T12:43:00.383Z","repository":{"id":57140642,"uuid":"88248028","full_name":"ecomfe/redux-optimistic-thunk","owner":"ecomfe","description":"redux-thunk like dispatching with optimistic UI supported","archived":false,"fork":false,"pushed_at":"2018-04-17T17:03:15.000Z","size":74,"stargazers_count":25,"open_issues_count":0,"forks_count":0,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-06-29T00:52:30.718Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ecomfe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-04-14T08:15:05.000Z","updated_at":"2020-08-11T02:45:04.000Z","dependencies_parsed_at":"2022-09-03T16:12:22.848Z","dependency_job_id":null,"html_url":"https://github.com/ecomfe/redux-optimistic-thunk","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/ecomfe/redux-optimistic-thunk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomfe%2Fredux-optimistic-thunk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomfe%2Fredux-optimistic-thunk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomfe%2Fredux-optimistic-thunk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomfe%2Fredux-optimistic-thunk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ecomfe","download_url":"https://codeload.github.com/ecomfe/redux-optimistic-thunk/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomfe%2Fredux-optimistic-thunk/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268392180,"owners_count":24243297,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-02T02:00:12.353Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-08-02T12:42:25.694Z","updated_at":"2025-08-02T12:43:00.371Z","avatar_url":"https://github.com/ecomfe.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# redux-optimistic-thunk\n\nredux-optimistic-thunk is a redux [redux-thunk](https://github.com/gaearon/redux-thunk) like middleware with optimistic UI supported.\n\n## Why this middleware\n\nThere are plenty of middleware for optimistic UI in redux ecosystem, such as [redux-optimistic](https://github.com/ForbesLindesay/redux-optimist) and [redux-optimistic-ui](https://github.com/mattkrick/redux-optimistic-ui), but both of them comes with several shortcomings:\n\n- They try to mix some properties in your plain action objects, this makes a high invasive design of your app, you should be aware of optimistic transactions everywhere and it's hard to conditionally decide to be optimistic or not.\n- They make developers manage transaction id themselves, if a complex business logic spreads over more than one functions, it is not a good experience to keep the transaction id in sync.\n- They take a commit-or-rollback term as its general design, however optimistic UI is not as simple as a transaction, for example, one may mark a newly created item `pending` when it's in an optimistic state to prevent user delete it. In fact both optimistic action and non-optimistic (actual) action are simple logics, they can be identical or different from each other.\n\nIn these cases, we decide to create a simpler and business-centric middleware to support optimistic UI development.\n\n## How this middleware works\n\nredux-optimistic-thunk is generally combined with 2 parts:\n\n1. A middleware that handles optimistic action.\n2. A higher order function to create reducer for optimistic UI.\n\nThe middleware consumes a special type of action (like redux-thunks consumes actions of type function), a optimistic action must be an array includes exactly 2 functions (thunks):\n\n1. The first function is exactly the same as what you will pass to redux-thunk, we call it a **actual thunk**.\n2. The second function is also a thunk accepting `(dispatch, getState, [extraArgument])`as arguments, but it's used for dispatching actions to produce optimistic states, we call it **optimistic thunk**.\n\nAs you see, all two functions are simple thunks so you can write whatever code for the well known redux-thunk middleware, redux-optimistic-thunk handles actual and optimistic thunks implicitly, creates state save point, applies optimistic actions, rollback them when actual thunk dispatches actual actions, it takes no effect on your action objects, and it is easy to remove optimistic UI (just delete the second item of array) or to conditionally choose whether to be optimistic (reuse the first item of array).\n\n## How to use\n\n### Create your store\n\nUse of redux-optimistic-thunk is quite simple, you should apply the provided middleware and reducers:\n\n```javascript\nimport {createStore, applyMiddleware} from 'redux';\nimport {optimisticThunk, createOptimisticReducer} from 'redux-optimistic-thunk';\n\nlet reducer = (state, action) =\u003e (action.type === 'PUSH' ? state.concat(action.value) : state);\nlet store = createStore(\n    createOptimisticReducer(reducer), // Wrap your reducer in createOptimisticReducer\n    [],\n    applyMiddleware(optimisticThunk()) // Apply middleware\n);\n```\n\nNote that different from redux-thunk, the export of redux-optimistic-thunk is a function which returns a middleware, so instead of `applyMiddleware(optimisticThunk)`, you should have a extra invocation `applyMiddleware(optimisticThunk())`, you can provide an `extraArgument` on invocation like `applyMiddleware(optimisticThunk(api))`.\n\nAfter middleware is applied, just wrap your reducer into the `createOptimisticReducer` function redux-optimistic-thunk provides, wrapped reducer adds optimistic state rollback ability to your store, it is a must.\n\n### Write your thunk\n\nSuppose our requirement is adding a newly submitted todo to list immediately (optimistically) before it is saved in server, however the optimistic todo cannot be deleted before it completes persistence:\n\n```javascript\n// action/todo.js\n\nimport {addLog} from './log';\nimport {warn} from './modal';\nimport {saveTodo} from '../api';\n\nlet newTodo = todo =\u003e ({type: 'NEW_TODO', todo: todo});\n\n// A optimistic action consists of 2 thunks\nlet createTodo = todo =\u003e [\n    // The first thunk (actual thunk) contains your actual business logic\n    async dispatch =\u003e {\n        // Any sync dispatches will apply first\n        dispatch(addLog(`Submitted ${todo.title} task`));\n\n        try {\n            // Async logic goes here\n            let createdTodo = await saveTodo(todo);\n\n            // The first async dispatch rollbacks actions produced by optimistic thunk,\n            // it will rollback your state to the point **after** \"Submitted xxx task\" log\n            dispatch(addLog(`Created ${createdTodo.title} task`));\n            dispatch(newTodo(createdTodo)); // No pending mark so the actually persisted todo can be deleted\n        }\n        catch (ex) {\n            dispatch(warn(`Failed to create ${todo.title} task`));\n        }\n    },\n\n    // The second thunk (optimistic thunk) dispatches actions to produce optimistic states\n    dispatch =\u003e {\n        // Optimistic thunk must be sync, any async dispatch throws error\n        dispatch(addLog(`Created ${todo.title} task`));\n        dispatch(newTodo({...todo, pending: true})); // Add a pending mark, disable the delete button if pending\n    }\n];\n```\n\nIt's simple, you just write 2 blocks of business logic, no extra properties to your plain action object, no transaction id and commit/revert signals.\n\n### Determine whether state is optimistic\n\nredux-optimistic-thunk adds an `optimistic` property to your state, this property is initialized as `false`.\n\nWhenever a plain object action is dispatched from an optimistic thunk, the `optimistic` property is set to `true`, it will return to `false` when all dispatched optimistic actions have been rollbacked, so it's possible to either use `getState().optimistic` in thunk/middleware or use `state.optimistic` in reducer to resolve whether the state is optimistic.\n\n## Detail\n\n### Actual thunk must make async dispatch\n\nAn async call to `dispatch` function provided to actual thunk is the only way to tell redux-optimistic-thunk to rollback optimistic state, so if your actual thunk - in some cases - does not call `dispatch` after the thunk function returns, the optimistic state **will never be rollbacked**, this can lead to a damaged state.\n\nThis can usually happen in lack of error handling:\n\n```javascript\nlet createTodo = todo =\u003e [\n    async dispatch =\u003e {\n        let createdTodo = await saveTodo(todo);\n\n        dispatch(newTodo(createdTodo));\n    },\n\n    dispatch =\u003e dispatch(newTodo({...todo, pending: true}));\n];\n```\n\nSince the actual thunk does not handle any errors from server response, it is possible that no async `dispatch` calls will be made if server returns a `50x` or `40x` response, in this case a optimistic pending todo will live in todo list forever, the entire application state is always optimistic.\n\nIf - for some reason - you cannot dispatch any action asynchronously in actual thunk, just call `dispatch` function provided to actual thunk without any argument, this `dispatch` function can properly \"swallow\" `undefined` action and correctly rollback optimistic states:\n\n```javascript\nlet createTodo = todo =\u003e [\n    async dispatch =\u003e {\n        try {\n            let createdTodo = await saveTodo(todo);\n\n            dispatch(newTodo(createdTodo));\n        }\n        catch (ex) {\n            // OK we don't want to handle errors, so just call dispatch with no argument\n            dispatch();\n        }\n    },\n\n    dispatch =\u003e dispatch(newTodo({...todo, pending: true}));\n];\n```\n\n### The order of middleware\n\nInternally when redux-optimistic-thunk rollbacks your state and re-applies actions, instead of calling the global `dispatch` function it invokes the `next` function in middleware, so any middleware placed before redux-optimistic-thunk will not receive any re-applied action.\n\nTo ensure all actions - either dispatched by user or re-applied by redux-optimistic-thunk - can be received, a middleware must be placed **after** `optimisticThunk()` in `applyMiddleware` function.\n\n### Special action types\n\nIn order to rollback state and mark state as optimistic, redux-optimistic-thunk will dispatch some special types of actions, these actions have a type prefixed with `@@redux-optimistic-thunk/`, it is encouraged that any middleware and reducers should ignore these special actions.\n\n### Nested thunk\n\nredux-optimistic-thunk keeps in track of the first async call to `dispatch` function provided to actual thunk and will rollback optimistic thunk in this point, even the first call is to dispatch a nested thunk (when combined with redux-thunk), the rollback will be landed, so any optimistic state produced from corresponding optimistic thunk will lost, this is currently by design.\n\n## Run example\n\nFrom running CLI example you could have an overview on how redux-optimistic-thunk manages your state:\n\n```shell\nnpm install\nnpm run start-cli\n```\n\nSee [test/cli/main.js](test/cli/main.js) for detail code.\n\nRunning react example will provide you a more practical app:\n\n```shell\nnpm install\nnpm run start-react\n```\n\nFollow the instructions on webpage to test optimistic UI effects.\n\n## Change Log\n\n### 2.0.0\n\n- Refreshed build with single rollup.\n- The es module version is now located at `dist/index.mjs`.\n- Add `\"sideEffects\": false` to `package.json`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fecomfe%2Fredux-optimistic-thunk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fecomfe%2Fredux-optimistic-thunk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fecomfe%2Fredux-optimistic-thunk/lists"}