{"_id":"@anyjunk/redux-offline","_rev":"5-e04061e4219351c1c86e166e9c203878","name":"@anyjunk/redux-offline","description":"Redux Offline-First Architecture","dist-tags":{"latest":"2.2.1"},"versions":{"1.1.0":{"name":"@anyjunk/redux-offline","version":"1.1.0","description":"Redux Offline-First Architecture","main":"lib/index.js","scripts":{"build":"babel src --out-dir lib --ignore **/__tests__/**","flow:start":"flow server","flow:stop":"flow stop","flow":"flow; test $? -eq 0 -o $? -eq 2","lint":"eslint src/","prepublish":"npm run lint && npm run flow && npm run test && npm run build","test":"jest","test:watch":"jest --watch","watch":"npm run build -- --watch"},"jest":{"rootDir":"src"},"repository":{"type":"git","url":"git+https://github.com/jevakallio/redux-offline.git"},"author":"","license":"MIT","bugs":{"url":"https://github.com/jevakallio/redux-offline/issues"},"homepage":"https://github.com/jevakallio/redux-offline#readme","devDependencies":{"babel-cli":"^6.18.0","babel-core":"^6.18.2","babel-eslint":"^7.1.1","babel-loader":"^6.2.8","babel-plugin-transform-class-properties":"^6.19.0","babel-plugin-transform-flow-strip-types":"^6.18.0","babel-plugin-transform-object-rest-spread":"^6.19.0","babel-preset-latest":"^6.16.0","babel-preset-react":"^6.16.0","eslint":"^3.17.1","eslint-config-formidable":"^2.0.1","eslint-plugin-babel":"^4.1.1","eslint-plugin-filenames":"^1.1.0","eslint-plugin-import":"^2.2.0","flow-bin":"^0.42.0","jest":"^19.0.2","react":"^15.4.2","redux":"^3.6.0","redux-logger":"^2.8.2"},"dependencies":{"redux-persist":"^4.5.0"},"peerDependencies":{"redux":">=3"},"gitHead":"6c1c278bcd1d1bcb4f31d0674e2c075a92a85c40","_id":"@anyjunk/redux-offline@1.1.0","_shasum":"f440235a93e0dd5f912a8706194d5aa15206e8bc","_from":".","_npmVersion":"3.10.10","_nodeVersion":"6.11.1","_npmUser":{"name":"anyjunk","email":"client-devs@anyjunk.co.uk"},"dist":{"shasum":"f440235a93e0dd5f912a8706194d5aa15206e8bc","tarball":"https://registry.npmjs.org/@anyjunk/redux-offline/-/redux-offline-1.1.0.tgz","integrity":"sha512-ci0yIyaYuCf4M8s042juocEIXnIVTm1L2L216RF7MWPisCO5u2u0rjA1g6s99ZToIE/9g3CFPPBFaoFK0JCu/A==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIGCS4P0jO3owMakkRZvKvHfxACIYHPQQG3hmyOsLpczSAiBnBq+veC68TtC0WNhZvDMowgw7/nBg5q1cY2VaV+2JfQ=="}]},"maintainers":[{"name":"anyjunk","email":"client-devs@anyjunk.co.uk"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/redux-offline-1.1.0.tgz_1501584732220_0.39075050316751003"}},"2.1.0":{"name":"@anyjunk/redux-offline","version":"2.1.0","description":"Redux Offline-First Architecture","main":"lib/index.js","scripts":{"build":"babel src --out-dir lib --ignore '**/__tests__/**'","flow:start":"flow server","flow:stop":"flow stop","flow":"flow; test $? -eq 0 -o $? -eq 2","lint":"eslint src/","prepublish":"npm run lint && npm run flow && npm run test && npm run build","test":"jest","test:watch":"jest --watch","watch":"npm run build -- --watch"},"jest":{"rootDir":"src"},"repository":{"type":"git","url":"git+https://github.com/anyjunk/redux-offline.git"},"author":"","license":"MIT","bugs":{"url":"https://github.com/anyjunk/redux-offline/issues"},"homepage":"https://github.com/anyjunk/redux-offline#readme","devDependencies":{"babel-cli":"^6.26.0","babel-core":"^6.26.0","babel-eslint":"^7.2.3","babel-plugin-transform-flow-strip-types":"^6.18.0","babel-plugin-transform-class-properties":"^6.24.1","babel-preset-latest":"^6.24.1","babel-preset-stage-3":"^6.24.1","eslint":"^4.6.1","eslint-config-airbnb-base":"^12.0.0","eslint-plugin-babel":"^4.1.1","eslint-plugin-import":"^2.7.0","flow-bin":"^0.42.0","jest":"^19.0.2","react":"^15.4.2","redux":"^3.6.0","redux-devtools-instrument":"^1.8.2","redux-logger":"^2.8.2","redux-persist-node-storage":"^1.0.2"},"dependencies":{"redux-persist":"^4.5.0"},"peerDependencies":{"redux":">=3"},"gitHead":"b2c3dff38e13b508137515a06ddd314a19f1f244","_id":"@anyjunk/redux-offline@2.1.0","_shasum":"c93215b6718ad37b6aca01b680ef562ff5763b2a","_from":".","_npmVersion":"3.10.10","_nodeVersion":"6.11.3","_npmUser":{"name":"anyjunk","email":"client-devs@anyjunk.co.uk"},"dist":{"shasum":"c93215b6718ad37b6aca01b680ef562ff5763b2a","tarball":"https://registry.npmjs.org/@anyjunk/redux-offline/-/redux-offline-2.1.0.tgz","integrity":"sha512-KO8IR+24cFX9+up5rXhwSLXtCP2WdaXkIurERRnbUDmO2osPXtTnQDSR9yP0OhFDKyHPKkaKcubnVXrZQc3Wxw==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQC0Xrf39wPdk2L3PvKkUpNVBRk35kHfoC9w0GZNGuGgIgIhALXWodkAi0r2tin78Z2UDLYguEa+AOUe8tGRc4numFYa"}]},"maintainers":[{"name":"anyjunk","email":"client-devs@anyjunk.co.uk"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/redux-offline-2.1.0.tgz_1506597592879_0.41939945914782584"}},"2.2.0":{"name":"@anyjunk/redux-offline","version":"2.2.0","description":"Redux Offline-First Architecture","main":"lib/index.js","scripts":{"build":"babel src --out-dir lib --ignore '**/__tests__/**'","flow:start":"flow server","flow:stop":"flow stop","flow":"flow; test $? -eq 0 -o $? -eq 2","lint":"eslint src/","prepublish":"npm run lint && npm run flow && npm run test && npm run build","prettier":"eslint src --fix","test":"jest","test:watch":"jest --watch","watch":"npm run build -- --watch"},"jest":{"rootDir":"src"},"repository":{"type":"git","url":"git+https://github.com/redux-offline/redux-offline.git"},"author":"","license":"MIT","bugs":{"url":"https://github.com/redux-offline/redux-offline/issues"},"homepage":"https://github.com/redux-offline/redux-offline#readme","devDependencies":{"babel-cli":"^6.26.0","babel-core":"^6.26.0","babel-eslint":"^7.2.3","babel-plugin-transform-class-properties":"^6.24.1","babel-plugin-transform-flow-strip-types":"^6.18.0","babel-preset-latest":"^6.24.1","babel-preset-stage-3":"^6.24.1","eslint":"^4.6.1","eslint-config-airbnb-base":"^12.0.0","eslint-config-prettier":"^2.4.0","eslint-plugin-babel":"^4.1.1","eslint-plugin-import":"^2.7.0","eslint-plugin-prettier":"^2.2.0","flow-bin":"^0.42.0","jest":"^19.0.2","prettier":"^1.6.1","react":"^15.4.2","redux":"^3.6.0","redux-devtools-instrument":"^1.8.2","redux-logger":"^2.8.2","redux-persist-node-storage":"^1.0.2"},"dependencies":{"redux-persist":"^4.5.0"},"peerDependencies":{"redux":">=3"},"gitHead":"8852434adfe5814353a6215ef04c2185daab5094","_id":"@anyjunk/redux-offline@2.2.0","_shasum":"fafbaca4350b679be9b786460c1c42724261f6ad","_from":".","_npmVersion":"3.10.10","_nodeVersion":"6.11.5","_npmUser":{"name":"anyjunk","email":"client-devs@anyjunk.co.uk"},"dist":{"shasum":"fafbaca4350b679be9b786460c1c42724261f6ad","tarball":"https://registry.npmjs.org/@anyjunk/redux-offline/-/redux-offline-2.2.0.tgz","integrity":"sha512-Mnop83TuS6TbVg5qlAtDg8zuI5OLDv/QuKnju3fV/IYZetYSQ6yDyQD50JF0HxWf3Tz4wFETOIzdSX8uSXFE8Q==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIQDDZ/nQK41Mf0QsQf/EW328uBKtyrvcqvUDA9he281kxQIgNEaovGAXoHGkZJWNT+71nrh+yZFrGC+meB4p2wdhTnA="}]},"maintainers":[{"name":"anyjunk","email":"client-devs@anyjunk.co.uk"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/redux-offline-2.2.0.tgz_1510766588799_0.8759102071635425"}},"2.2.1":{"name":"@anyjunk/redux-offline","version":"2.2.1","description":"Redux Offline-First Architecture","main":"lib/index.js","types":"./typings.d.ts","scripts":{"build":"babel src --out-dir lib --ignore '**/__tests__/**'","flow:start":"flow server","flow:stop":"flow stop","flow":"flow; test $? -eq 0 -o $? -eq 2","lint":"eslint src/","prepublish":"npm run lint && npm run flow && npm run test && npm run build","prettier":"eslint src --fix","test":"jest","test:watch":"jest --watch","watch":"npm run build -- --watch"},"jest":{"rootDir":"src"},"repository":{"type":"git","url":"git+https://github.com/redux-offline/redux-offline.git"},"author":"","license":"MIT","bugs":{"url":"https://github.com/redux-offline/redux-offline/issues"},"homepage":"https://github.com/redux-offline/redux-offline#readme","devDependencies":{"@types/redux":"^3.6.0","babel-cli":"^6.26.0","babel-core":"^6.26.0","babel-eslint":"^7.2.3","babel-plugin-transform-class-properties":"^6.24.1","babel-plugin-transform-flow-strip-types":"^6.18.0","babel-plugin-transform-runtime":"^6.23.0","babel-preset-latest":"^6.24.1","babel-preset-stage-3":"^6.24.1","eslint":"^4.6.1","eslint-config-airbnb-base":"^12.0.0","eslint-config-prettier":"^2.4.0","eslint-plugin-babel":"^4.1.1","eslint-plugin-import":"^2.7.0","eslint-plugin-prettier":"^2.2.0","flow-bin":"^0.42.0","jest":"^19.0.2","prettier":"^1.6.1","react":"^15.4.2","redux":"^3.6.0","redux-devtools-instrument":"^1.8.2","redux-logger":"^2.8.2","redux-persist-node-storage":"^1.0.2"},"dependencies":{"babel-runtime":"^6.26.0","redux-persist":"^4.5.0"},"peerDependencies":{"redux":">=3"},"gitHead":"48f755c3a2ab69c6497078623a4dc2bfe93da4ed","_id":"@anyjunk/redux-offline@2.2.1","_shasum":"5b2018baae5d31cb3608f814354ff343b452758d","_from":".","_npmVersion":"3.10.10","_nodeVersion":"6.12.3","_npmUser":{"name":"anyjunk","email":"client-devs@anyjunk.co.uk"},"dist":{"shasum":"5b2018baae5d31cb3608f814354ff343b452758d","tarball":"https://registry.npmjs.org/@anyjunk/redux-offline/-/redux-offline-2.2.1.tgz","integrity":"sha512-RG4Dc3XtAJbc9HJUj+dK1/bFwPuX6mniv6Xb1LtXiEROAQDEU33ryRGLmQNyKBwQAZ5MOtrkPBRm9ZWixtmINg==","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQCrruBqO8/OxlzoHV6EUdACBWxa/3FRZlMLkC/wKiKafwIhAPF9YVC4YDVxhtzbAaFGBAPUoOYdKFJZDdE7iPBImbtd"}]},"maintainers":[{"name":"anyjunk","email":"client-devs@anyjunk.co.uk"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/redux-offline-2.2.1.tgz_1517830379402_0.20454688393510878"}}},"readme":"<p>\n  <img alt=\"redux-offline\" src=\"docs/logo.png\" width=\"300\"></img>\n</p>\n<p>\n  <a title='License' href=\"https://raw.githubusercontent.com/redux-offline-team/redux-offline/master/LICENSE\" height=\"18\">\n    <img src='https://img.shields.io/badge/license-MIT-blue.svg' />\n  </a>\n  <a href=\"https://badge.fury.io/js/redux-offline\">\n    <img src=\"https://badge.fury.io/js/redux-offline.svg\" alt=\"npm version\" height=\"18\">\n  </a>\n  <a href=\"https://travis-ci.org/redux-offline-team/redux-offline\">\n    <img src=\"https://travis-ci.org/redux-offline-team/redux-offline.svg?branch=master\" alt=\"travis\" height=\"18\">\n  </a>\n</p>\n\nPersistent Redux store for _Reasonaboutable_:tm: Offline-First applications, with first-class support for optimistic UI. Use with React, React Native, or as standalone state container for any web app.\n\n_To get started, take a moment to read through the **[Offline Guide](#offline-guide)** to understand the architecture and tradeoffs behind Redux Offline, and for further context why Offline matters, read [this blog post](https://hackernoon.com/introducing-redux-offline-offline-first-architecture-for-progressive-web-applications-and-react-68c5167ecfe0)_\n\n## Contents\n\n* [Quick start](#quick-start)\n* [Offline Guide](#offline-guide)\n* [Configuration](#configuration)\n* [Contributing](#contributing)\n* [Miscellanea](#miscellanea)\n\n## Full disclosure\n\nRedux Offline is now being maintained by a community driven team. The new versions of the library will now be available under the npm organization `@redux-offline`. Big thank you to [@jevakallio](https://github.com/jevakallio) for creating this amazing library in the first place.\n\n## Quick start\n\n##### 1. Install with npm (or [Yarn](https://yarnpkg.com))\n```diff\n- npm install --save redux-offline\n+ npm install --save @redux-offline/redux-offline\n```\n\n##### 2. Add the `offline` [store enhancer](http://redux.js.org/docs/Glossary.html#store-enhancer) with `compose`\n```diff\n\n- import { applyMiddleware, createStore } from 'redux';\n+ import { applyMiddleware, createStore, compose } from 'redux';\n- import { offline } from 'redux-offline';\n+ import { offline } from '@redux-offline/redux-offline';\n- import offlineConfig from 'redux-offline/lib/defaults';\n+ import offlineConfig from '@redux-offline/redux-offline/lib/defaults';\n\n// ...\n\nconst store = createStore(\n  reducer,\n  preloadedState,\n-  applyMiddleware(middleware)\n+  compose(\n+    applyMiddleware(middleware),\n+    offline(offlineConfig)\n+  )\n);\n```\n\nSee [Configuration](#configuration) for overriding default configurations.\n\nLooking for `createOfflineStore` from redux-offline 1.x? See migration instructions in the [2.0.0 release notes](https://github.com/redux-offline-team/redux-offline/releases/tag/v2.0.0).\n\n##### 3. Decorate actions with offline metadata\n\n```js\nconst followUser = userId => ({\n  type: 'FOLLOW_USER_REQUEST',\n  payload: { userId },\n  meta: {\n    offline: {\n      // the network action to execute:\n      effect: { url: '/api/follow', method: 'POST', body: { userId } },\n      // action to dispatch when effect succeeds:\n      commit: { type: 'FOLLOW_USER_COMMIT', meta: { userId } },\n      // action to dispatch if network action fails permanently:\n      rollback: { type: 'FOLLOW_USER_ROLLBACK', meta: { userId } }\n    }\n  }\n});\n```\n\nRead the [Offline Guide](#offline-guide) to understand how effects are executed, and how the actions are dispatched.\n\n##### 4. (React Native Android) Ask permission to read network status\n\nIf writing a native app for Android, you'll need to make sure to request the permission to access network state in your `AndroidManifest.xml`:\n\n```xml\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n```\n\n\n## Offline Guide\n\nMaking offline-friendly apps is not rocket science, but to make them work well involves dealing with finicky details around persisting state, resilience against flaky networks, optimistically updating user interface state, reliably reverting it back in case of failures, synchronising state in the background, and managing the evolution of the persistent state over long, long periods of time.\n\n**Redux Offline** is a battle-tested offline-first architecture, and an _experimental_ library that implements it. To make use of the library, it'll be helpful to understand the architecture behind it.\n\n### Progressive Web Apps\n\nRedux Offline helps you with offline state management, but it **does not** automatically make your web site available offline. For caching assets (HTML pages, scripts, images, and other resources) your website needs to implement a ServiceWorker. To get started with PWAs and React, [this article provides great list of resources](https://medium.com/@addyosmani/progressive-web-apps-with-react-js-part-3-offline-support-and-network-resilience-c84db889162c) to begin with.\n\n### Persistence is key\nIn order to be able to render meaningful content when the user opens your application offline, your application state needs to be persisted to disk.\n\nInstead of reinventing the wheel, Redux Offline uses the excellent [redux-persist](https://github.com/rt2zz/redux-persist/tree/v4) library. Your Redux store is saved to disk on every change, and reloaded automatically on startup. By default, browser environments will use [IndexedDB](https://developer.mozilla.org/en/docs/Web/API/IndexedDB_API) or WebSQL/localStorage fallbacks via [localForage](https://github.com/localForage/localForage), and [AsyncStorage](https://facebook.github.io/react-native/docs/asyncstorage.html) in React Native.\n\nYou can [configure every aspect of how your state is persisted](#configuration).\n\n### That's all she wrote\n\nPersisting and rehydrating state (which is a term we use for reading the state back from the disk and into our store) will get us **Read**-resilience. Our app will work offline as long as the user only wants to read from the state. We also want to support **Write**-resilience: The user should be able to do (some) actions while offline, and be able to safely assume that they will eventually be reconciled and sent to our backend.\n\nIn order to support Write-resilience, we will store all network-bound actions in a queue inside our store. Redux Offline creates a state subtree called `offline`, where among other internal state needed by the library, it manages an array called `outbox`.\n\nTo be able to perform the network-bound actions after we come back online, we need to store all necessary data to perform the action, and metadata about what should happen afterwards. Redux Offline understands the following metadata:\n```diff\ntype OfflineAction = {\n  type: string,\n  payload: any,\n+ meta: {\n+   offline: {\n+     effect: any,\n+     commit: Action,\n+     rollback: Action\n+   }\n+ }\n```\n\n* `meta.offline.effect` is any data you want to send to the reconciler\n* `meta.offline.commit` action will be fired once the network effect has been successfully sent\n* `meta.offline.rollback` action will be fired if the network effect **permanently** fails (does not count network-related failures, which will be automatically retried).\n\n### Optimism will get you places\n\nWhen the initial action has been dispatched, you can update your application state in your reducers as you normally would.\n\nA common pattern for offline-friendly apps is to *optimistically update  UI state*. In practice, this means that as soon as user performs an action, we update the UI to look as if the action had already succeeded. This makes our applications resilient to network latency, and improves the perceived performance of our app.\n\nWhen we optimistically update state, we need to ensure that if the action does permanently fail, the user is appropriately notified and the application state is rolled back. To allow you this opportunity, Redux Offline will fire the action you specified in `meta.offline.rollback`. The error object returned by the effects reconciler will be set as the payload.\n\nAn example of an optimistic update:\n```js\nconst action = userId => ({\n  type: 'FOLLOW_USER',\n  payload: { userId },\n  meta: {\n    offline: {\n      effect: //...,\n      rollback: { type: 'FOLLOW_USER_ROLLBACK', meta: { userId }}\n     }\n  }\n});\n\n// optimistically update the state, revert on rollback\nconst followingUsersReducer = (state, action) {\n  switch(action.type) {\n    case 'FOLLOW_USER':\n      return { ...state, [action.payload.userId]: true };\n    case 'FOLLOW_USER_ROLLBACK':\n      return omit(state, [action.meta.userId]);\n    default:\n      return state;\n  }\n}\n```\n\n### A pessimist is never disappointed\n\nSometimes it's important that the user knows that the action actually went through, so you can't optimistically update your state until the effect has been executed. Or sometimes, in order to render the final UI state, you need some data from the server response. For these cases, you can use the `meta.offline.commit` action:\n\n```js\nconst completeOrder = (orderId, lineItems) => ({\n  type: 'COMPLETE_ORDER',\n  payload: { orderId, lineItems },\n  meta: {\n    offline: {\n      effect: //...,\n      commit: { type: 'COMPLETE_ORDER_COMMIT', meta: { orderId }},\n      rollback: { type: 'COMPLETE_ORDER_ROLLBACK', meta: { orderId }}\n     }\n  }\n});\n\nconst ordersReducer = (state, action) {\n  switch(action.type) {\n    case 'COMPLETE_ORDER':\n      return {\n        ...state,\n        submitting: {...state.submitting, [action.payload.orderId]: true\n      };\n    case 'COMPLETE_ORDER_COMMIT':\n      return {\n        ...state,\n        receipts: { ...state.receipts, [action.meta.orderId]: action.payload },\n        submitting: omit(state.submitting, [action.meta.orderId])\n      };\n    case 'COMPLETE_ORDER_ROLLBACK':\n      return {\n        ...state,\n        error: action.payload,\n        submitting: omit(state.submitting, [action.meta.orderId])\n      };\n    default:\n      return state;\n  }\n}\n```\n\n### Executor of our will\n\nThe last part of the offline metadata is `meta.offline.effect`. This property can contain anything, and will be passed as-is to the effects reconciler.\n\nThe **effects reconciler** is a function that you pass to offline enhancer configuration, whose responsibility it is to take the effect payload, send it over the network, and return a Promise that resolves if sending was successful or rejects if the sending failed. The method is passed the full action as a second parameter:\n\n```js\ntype EffectsReconciler = (effect: any, action: OfflineAction) => Promise<any>\n```\n\nThe default reconciler is simply a paper-thin wrapper around [fetch](https://developer.mozilla.org/en/docs/Web/API/Fetch_API) that rejects non-OK HTTP status codes, and assumes the response will be valid JSON.\n```js\n  const effectReconciler = ({url, ...opts}) =>\n    fetch(url, opts).then(res => res.ok\n      ? res.json()\n      : Promise.reject(res.text().then(msg => new Error(msg))));\n```\nSo the default effect format expected by the reconciler is something like:\n```js\n{\n  type: 'ACTION',\n  meta: {\n    offline: {\n      effect: { url: '/api/endpoint', method: 'POST'}\n    }\n  }\n}\n```\n\nThat said, you'll probably want to [use your own method](#change-how-network-requests-are-made) - it can be anything, as long as it returns a Promise.\n\n### Is this thing even on?\n\nA library that aims to support offline usage, it would be useful to know whether or not the device is online or offline. Unfortunately, network availability is not a binary \"Yes\" or \"No\": It can also be \"Yes, but not really\". The network receiver on your mobile device may report connectivity, but if you can't reach the remote server, are we really connected?\n\nRedux Offline uses the browser [Network Information APIs](https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API) and React Native [NetInfo](https://facebook.github.io/react-native/docs/netinfo.html) to be notified when the device *thinks* it's online to orchestrate synchronisation and retries. The current reported online state is stored in your store state as boolean `state.offline.online` and can be used to display a network indicator in your app, if desired.\n\nSometimes, it's more reliable to check network connectivity by actually making sure you can exchange data with a remote server by periodically making a HEAD request, or keeping an open WebSocket connection with a heartbeat. This, too, [can be configured](#change-how-network-status-is-detected).\n\n\n### Giving up is hard to do\n\nNetworks are flaky. Your backend could be down. Sometimes, when the moon is in waxing crescent and the sixth-degree leylines are obstructed by passing birds, things mysteriously fail. If you are processing your offline actions queue in serial, you will need a reliable mechanism to decide when to retry the requests, and when to give up to prevent blocking the rest of the queue from being flushed.\n\nBuilding an offline-friendly app, you should never give up because a *network* connection failed. You *may* want to give up if the server reports a *server error*. And if the server tells you that your *request cannot be processed*, you need to give up immediately.\n\nModelled after this principle, the default discard strategy is:\n* If server was not reached, always retry\n* If server responded with HTTP `4xx` client error, always discard\n* If server responded with HTTP `5xx` server error, retry with a decaying schedule configured by the [retry strategy](#change-how-network-requests-are-retried).\n\nIf your backend doesn't conform to this standard, or you've [changed the effects reconciler](#change-how-network-requests-are-made) to return errors that don't expose a HTTP `status` field, you'll want to [configure the error detection strategy](#change-how-irreconcilable-errors-are-detected), too.\n\nWhen a message is discarded, the `meta.offline.rollback` action defined in the message metadata is fired, and you can respond accordingly.\n\n\n### And if you don't at first succeed, try, try again\n\nWhen a network request has failed, and you've [chosen not to discard the message](#giving-up-is-hard-to-do), you need to decide when to retry the request. If your requests are failing due to an overloaded backend, retrying too often will make the problem worse and effectively DDoS your own service. Never kick a man when he's down.\n\nBy default, we will always retry the first message in the queue when the [network detector](#is-this-thing-even-on) reports a change from offline to online. Otherwise, we will retry the request on a decaying schedule:\n* After 1 seconds\n* After 5 seconds\n* After 15 seconds\n* After 30 seconds\n* After 1 minute\n* After 3 minutes\n* After 5 minutes\n* After 10 minutes\n* After 30 minutes\n* After 1 hour\n\nAfter these 10 timed attempts, if the message is still failing due to a server error, it will be discarded.\n\nRetrying a request for this long may seem excessive, and for some use cases it can be. You can [configure the retry strategy](#change-how-network-requests-are-retried) to suit yours.\n\nThe reason the default behaviour is to desperately try to make the requests succeed is that we really, really want to avoid having to deal with conflict resolution...\n\n\n## Configuration\n\n\n### Configuration object\n\nRedux Offline supports the following configuration properties:\n```js\nexport type Config = {\n  detectNetwork: (callback: NetworkCallback) => void,\n  effect: (effect: any, action: OfflineAction) => Promise<*>,\n  retry: (action: OfflineAction, retries: number) => ?number,\n  discard: (error: any, action: OfflineAction, retries: number) => boolean|Promise<boolean>,\n  defaultCommit: { type: string },\n  defaultRollback: { type: string },\n  persist: (store: any) => any,\n  persistOptions: {},\n  persistCallback: (callback: any) => any,\n  persistAutoRehydrate: (config: ?{}) => (next: any) => any,\n  offlineStateLens: (state: any) => { get: OfflineState, set: (offlineState: ?OfflineState) => any }\n};\n```\n\n#### Passing configuration to the enhancer\nThe `offline` store enhancer takes the [configuration object](#configuration-object) as a final parameter:\n```diff\n- import { offline } from 'redux-offline';\n+ import { offline } from '@redux-offline/redux-offline';\n- import defaultConfig from 'redux-offline/lib/defaults';\n+ import defaultConfig from '@redux-offline/redux-offline/lib/defaults';\n\nconst store = createStore(\n  reducer,\n  preloadedState,\n-  middleware\n+  compose(middleware, offline(defaultConfig))\n);\n```\n\n#### Overriding default properties\nYou can override any individual property in the default configuration:\n```diff\n- import { offline } from 'redux-offline';\n+ import { offline } from '@redux-offline/redux-offline';\n- import defaultConfig from 'redux-offline/lib/defaults';\n+ import defaultConfig from '@redux-offline/redux-offline/lib/defaults';\n\nconst customConfig = {\n  ...defaultConfig,\n  effect: (effect, _action) => Api.send(effect)\n}\n\nconst store = createStore(\n  reducer,\n  preloadedState,\n-  middleware\n+  compose(middleware, offline(customConfig))\n);\n```\n\n#### Only import what you need\nThe reason for default config is defined as a separate import is, that it pulls in the [redux-persist](https://github.com/rt2zz/redux-persist/tree/v4) dependency and a limited, but non-negligible amount of library code. If you want to minimize your bundle size, you'll want to avoid importing any code you don't use, and bring in only the pieces you need:\n\n```diff\n- import { offline } from 'redux-offline';\n+ import { offline } from '@redux-offline/redux-offline';\n- import retry from 'redux-offline/lib/defaults/retry';\n+ import retry from '@redux-offline/redux-offline/lib/defaults/retry';\n- import discard from 'redux-offline/lib/defaults/discard';\n+ import discard from '@redux-offline/redux-offline/lib/defaults/discard';\n\nconst myConfig = {\n  retry,\n  discard,\n  effect: (effect, action) => MyCustomApiService.send(effect, action),\n  detectNetwork: (callback) => MyCustomPingService.startPing(callback),\n  persist: (store) => MyCustomPersistence.persist(store)\n};\n\nconst store = createStore(\n  reducer,\n  preloadedState,\n-  middleware\n+  compose(middleware, offline(myConfig))\n myConfig\n);\n```\n\n\n### I want to...\n\n#### Change how network requests are made\n\nProbably the first thing you will want to do is to replace the default `fetch` effects handler. Do this by overriding `config.effect`:\n```js\nconst config = {\n  effect: (effect, action) => {\n    console.log(`Executing effect for ${action.type}`);\n    return MyApi.send(effect)\n  }\n}\n```\n\nThe first parameter is whatever value is set in `action.meta.offline.effect`. The second parameter is the full action, which may be useful for context. The method is expected to return a Promise. The full signature of the effect handler is: `(effect: any, action: OfflineAction) => Promise<any>`.\n\n\n#### Change how state is saved to disk\n\nBy default, persistence is handled by [redux-persist](https://github.com/rt2zz/redux-persist/tree/v4). The recommended way of customizing\npersistence is to configure redux-persist. You can pass any valid configuration\nto redux-persist by defining it `config.persistOptions`:\n```js\nconst config = {\n  persistOptions: { /*...*/ }\n};\n```\n\nYou can pass the callback for redux-persist as well. This function would be called when rehydration is complete. It's useful if you want to delay rendering until rehydration is complete. You can define it in `config.persistCallback`:\n```js\nconst config = {\n  persistCallback: () => { /*...*/ }\n};\n```\n\nYou can pass your persistAutoRehydrate method. For example in this way you can use the default rehydrator in debug mode, logging all actions before the rehydrate event.\n```js\nimport { autoRehydrate } from 'redux-persist';\n\nconst config = {\n  persistAutoRehydrate: () => autoRehydrate({log: true})\n};\n```\n\nIf you want to replace redux-persist entirely **(not recommended)**, you can override `config.persist`. The function receives the store instance as a first parameter, and is responsible for setting any subscribers to listen for store changes to persist it.\n```js\nconst config = {\n  persist: (store) =>\n    store.subscribe(() => console.log(store.getState()))\n  )\n}\n```\n\nIf you override `config.store`, you will also need to manage the rehydration of your state manually.\n\n#### Change how network status is detected\n\nTo replace the default network status detector, override the `config.detectNetwork` method:\n```js\nconst config = {\n  detectNetwork: callback => MyCustomDetector.on('change', callback)\n}\n```\n\nThe function is passed a callback, which you should call with boolean `true` when the app gets back online, and `false` when it goes offline.\nAdditionally you can call it with an object containing as props `online` and `netInfo`. The `online` is a boolean that defines whether there's connection or not,\nthe `netInfo` is an optional object containing details about the current network.\n \nThe default detectNetwork.js provides an object with `online` as the only property.\n\nThe default detectNetwork.native.js provides both the `online` and the `netInfo` props following `react-native` netInfo possible values.\nThe payload object would follow the following example:\n```js\n/**\n* netInfo reach values follow react-native's NetInfo values\n* Cross-platform: ['none', 'wifi', 'cellular', 'unknown']\n* Android: ['bluetooth', 'ethernet', 'wimax']\n*/\nconst payload = {\n  online: true, // determines the connection status\n  netInfo: {\n    reach: 'wifi', // network reach as provided by react native\n    isConnectionExpensive: false // whether connection is metered (only supported by android)\n  }\n};\n```\n\n#### Change how irreconcilable errors are detected\n\nActions in the queue are by default discarded when a server returns\na HTTP `4xx` error. To change this, set override the `config.discard` method:\n```js\nconst config = {\n  discard: (error, action, retries) => error.permanent || retries > 10;\n}\n```\n\nThe method receives the Error returned by the effect reconciler, the action being processed, and a number representing how many times the action has been retried. If the method returns `true`, the action will be discarded; `false`, and it will be retried. The full signature of the method is `(error: any, action: OfflineAction, retries: number) => boolean`. Alternatively, you can return a Promise object that resolve to a boolean, allowing you to detect when to discard asynchronously (for example, doing a request to a server to refresh a token and try again).\n\n#### Change how network requests are retried\n\nBy default, sending actions is retried on a decaying schedule starting with retries every few seconds, eventually slowing down to an hour before the last retry. These retry delays only apply to scenarios where the device reports being online but the server cannot be reached, or the server is reached but is responding with a non-permanent error.\n\nTo configure the retry duration, override `config.retry`:\n```js\nconst config = {\n  retry: (action, retries) => action.meta.urgent ? 100 : 1000 * (retries + 1)\n}\n```\n\nThe function receives the action and a number representing how many times the\naction has been retried, and should reply with a number representing the amount\nof milliseconds to wait until the next retry. If this method returns `null` or\n`undefined`, the action will not be retried until the next time the app comes\nonline, is started, or you manually fire an `Offline/SEND` action.\n\n#### Change how errors are handled\n\nGranular error handling is not yet implemented. You can use discard/retry, and\nif necessary to purge messages from your queue, you can filter `state.offline.outbox`\nin your reducers. Official support coming soon.\n\n#### Synchronise my state while the app is not open\n\nBackground sync is not yet supported. Coming soon.\n\n#### Use an [Immutable](https://facebook.github.io/immutable-js/) store\n\nThe `offline` state branch created by Redux Offline needs to be a vanilla JavaScript object.\nIf your entire store is immutable you should check out [`redux-offline-immutable-config`](https://github.com/anyjunk/redux-offline-immutable-config) which provides drop-in configurations using immutable counterparts and code examples.\nIf you use Immutable in the rest of your store, but the root object, you should not need extra configurations.\n\n#### Change where the offline state is stored\n\nBy default the offline state is stored in `state.offline`. This can be changed using `config.offlineStateLens()`. Refer to the [default implementation](https://github.com/redux-offline-team/redux-offline/blob/master/src/defaults/offlineStateLens.js) for how this might be done.\n\n#### Choose where the offline middleware is added\n\nBy default, the offline middleware is inserted right before the offline store enhancer as part of its own middleware chain. If you want more control over where the middleware is inserted, consider using the alternative api, `createOffline()`.\n\n```js\nimport { createOffline } from \"@redux-offline/redux-offline\";\nconst { middleware, enhanceReducer, enhanceStore } = createOffline(config);\nconst store = createStore(\n  enhanceReducer(rootReducer),\n  initialStore,\n  compose(applyMiddleware(middleware), enhanceStore)\n);\n```\n\n#### Empty the outbox\n\nIf you want to drop any unresolved offline actions, when a user logs off for instance, dispatch a reset state event as follows:\n\n```js\nimport { RESET_STATE } from \"@redux-offline/redux-offline/lib/constants\";\nstore.dispatch({ type: RESET_STATE });\n```\n\n## Contributing\n\nImprovements and additions welcome. For large changes, please submit a discussion issue before jumping to coding; we'd hate you to waste the effort.\n\nIn lieu of a formal style guide, follow the included eslint rules, and use Prettier to format your code.\n\n## Miscellanea\n\n### Prior art\n\nRedux Offline is a distillation of patterns discovered while building apps using previously existing libraries:\n\n* Forbes Lindesay's [redux-optimist](https://github.com/ForbesLindesay/redux-optimist)\n* Zack Story's [redux-persist](https://github.com/rt2zz/redux-persist/tree/v4)\n\nWithout their work, Redux Offline wouldn't exist. If you like the ideas behind Redux Offline, but want to build your own stack from lower-level components, these are good places to start.\n\n### License\n\nMIT\n","maintainers":[{"name":"anyjunk","email":"client-devs@anyjunk.co.uk"}],"time":{"modified":"2022-06-12T14:43:22.171Z","created":"2017-08-01T10:52:13.621Z","1.1.0":"2017-08-01T10:52:13.621Z","2.1.0":"2017-09-28T11:19:54.273Z","2.2.0":"2017-11-15T17:23:10.200Z","2.2.1":"2018-02-05T11:33:00.874Z"},"homepage":"https://github.com/redux-offline/redux-offline#readme","repository":{"type":"git","url":"git+https://github.com/redux-offline/redux-offline.git"},"bugs":{"url":"https://github.com/redux-offline/redux-offline/issues"},"license":"MIT","readmeFilename":"README.md"}