|
1 | 1 | # react-native-offline-api
|
2 | 2 |
|
| 3 | +Simple, customizable, offline-first API wrapper for react-native. |
| 4 | + |
| 5 | +Easily write offline-first react-native applications with your own REST API. This module supports every major features for network requests : middlewares, fine-grained control over caching logic, custom caching driver... |
| 6 | + |
| 7 | +## Table of contents |
| 8 | + |
| 9 | +- [react-native-offline-api](#react-native-offline-api) |
| 10 | + - [Table of contents](#table-of-contents) |
| 11 | + - [Installation](#installation) |
| 12 | + - [How to use](#how-to-use) |
| 13 | + - [Setting up your global API options](#setting-up-your-global-api-options) |
| 14 | + - [Declaring your services definitions](#declaring-your-services-definitions) |
| 15 | + - [Firing your first request](#firing-your-first-request) |
| 16 | + - [Methods](#methods) |
| 17 | + - [API options](#api-options) |
| 18 | + - [Services options](#services-options) |
| 19 | + - [Fetch options](#fetch-options) |
| 20 | + - [Path and query parameters](#path-and-query-parameters) |
| 21 | + - [Middlewares](#middlewares) |
| 22 | + - [Using your own driver for caching](#using-your-own-driver-for-caching) |
| 23 | + - [Types](#types) |
| 24 | + - [Roadmap](#roadmap) |
| 25 | + |
| 26 | +## Installation |
| 27 | + |
| 28 | +``` |
| 29 | +npm install --save react-native-offline-api # with npm |
| 30 | +yarn add react-native-offline-api # with yarn |
| 31 | +``` |
| 32 | + |
| 33 | +## How to use |
| 34 | + |
| 35 | +Since this plugin is a fully-fledged wrapper and not just a network helper, you need to set up your API configuration. |
| 36 | + |
| 37 | +### Setting up your global API options |
| 38 | + |
| 39 | +Here's an example : |
| 40 | + |
| 41 | +```javascript |
| 42 | +const API_OPTIONS = { |
| 43 | + domains: { default: 'http://myapi.tld', staging: 'http://staging.myapi.tld' }, |
| 44 | + prefixes: { default: '/api/v1', apiV2: '/api/v2' }, |
| 45 | + debugAPI: true, |
| 46 | + printNetworkRequests: true |
| 47 | +}; |
| 48 | +``` |
| 49 | + |
| 50 | +Here, we have set up the wrapper so it can use 2 different domains, a production API (the default one) and a staging API. |
| 51 | + |
| 52 | +We also have 2 different prefixes, so, if you're versioning your APIs by appending `/v2` in your URLs for example, you'll be able to easily request each versions. Please note that this is totally optional. |
| 53 | + |
| 54 | +**[Check out all the API options here](#api-options)** |
| 55 | + |
| 56 | +### Declaring your services definitions |
| 57 | + |
| 58 | +From now on, **we'll call all your API's endpoint services**. Now that you have set up your options, you need to declare your services. Easy peasy : |
| 59 | + |
| 60 | +```javascript |
| 61 | +const API_SERVICES = { |
| 62 | + articles: { |
| 63 | + path: 'articles', |
| 64 | + }, |
| 65 | + documents: { |
| 66 | + domain: 'staging', |
| 67 | + prefix: 'apiV2', |
| 68 | + path: 'documents/:documentId', |
| 69 | + disableCache: true |
| 70 | + }, |
| 71 | + login: { |
| 72 | + path: 'login', |
| 73 | + method: 'POST', |
| 74 | + expiration: 5 * 60 * 1000 |
| 75 | + } |
| 76 | +}; |
| 77 | +``` |
| 78 | + |
| 79 | +Here, we declared 3 services : |
| 80 | + |
| 81 | +* `articles` that will fetch data from `http://myapi.tld/api/v1/articles` |
| 82 | +* `documents` that will fetch data from `http://staging.myapi.tld/api/v2/documents/:documentId` and won't cache anything |
| 83 | +* `login` that will make a `POST` request on `http://myapi.tld/api/v1/login` with a 5 minutes caching |
| 84 | + |
| 85 | +These are just examples, **there are much more options for your services, [check them out here](#services-options).** |
| 86 | + |
| 87 | +### Firing your first request |
| 88 | + |
| 89 | +Now that we have our API options and services configured, let's call our API ! |
| 90 | + |
| 91 | +```javascript |
| 92 | +import React, { Component } from 'react'; |
| 93 | +import OfflineFirstAPI from 'react-native-offline-api'; |
| 94 | + |
| 95 | +// ... API and services configurations |
| 96 | + |
| 97 | +const api = new OfflineFirstAPI(API_OPTIONS, API_SERVICES); |
| 98 | + |
| 99 | +export default class demo extends Component { |
| 100 | + |
| 101 | + componentDidMount () { |
| 102 | + this.fetchSampleData(); |
| 103 | + } |
| 104 | + |
| 105 | + async fetchSampleData () { |
| 106 | + try { |
| 107 | + const request = await api.fetch( |
| 108 | + 'documents', |
| 109 | + { |
| 110 | + pathParameters: { documentId: 'xSfdk21' } |
| 111 | + } |
| 112 | + ); |
| 113 | + console.log('Our fetched document data', request); |
| 114 | + } catch (err) { |
| 115 | + // Handle any error |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + render () { |
| 120 | + // ... |
| 121 | + } |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +In this short example, we're firing a `GET` request on the path `http://staging.myapi.tld/api/v2/documents/xSfdk21`. If you don't understand how this path is constructed, see [path and query parameters](#path-and-query-parameters). |
| 126 | + |
| 127 | +A couple of notes : |
| 128 | + |
| 129 | +* The `fetch` and `fetchHeaders` methods are promises, which means you can either use `async/await` or `fetch().then().catch()` if you prefer. |
| 130 | +* You can instantiate `OfflineFirstAPI` without `API_OPTIONS` and/or `API_SERVICES` and set them later with `api.setOptions` and `api.setServices` methods if the need arises. |
| 131 | + |
| 132 | +## Methods |
| 133 | + |
| 134 | +Name | Description | Parameters | Return value |
| 135 | +------ | ------ | ------ | ------ |
| 136 | +`fetch` | Fires a network request to one of your service with additional options, see [fetch options](#fetch-options) | `service: string, options?: IFetchOptions` | `Promise<any>` |
| 137 | +`fetchHeaders` | Just like `fetch` but only returns the HTTP headers of the reponse | `service: string, options?: IFetchOptions` | `Promise<any>` |
| 138 | +`clearCache` | Clears all the cache, or just the one of a specific service | `service?: string` | `Promise<void>` |
| 139 | +`setOptions` | Sets or update the API options of the wrapper | `options: IAPIOptions` | `void` |
| 140 | +`setServices` | Sets or update your services definitions | `services: IAPIServices` | `void` |
| 141 | +`setCacheDriver` | Sets or update your custom cache driver, see [using your own driver for caching](#using-your-own-driver-for-caching) | `driver: IAPIDriver` | `void` |
| 142 | + |
| 143 | +## API options |
| 144 | + |
| 145 | +These are the global options for the wrapper. Some of them can be overriden at the service definition level, or with the `option` parameter of the `fetch` method. **Only `domains` and `prefixes` are required.** |
| 146 | + |
| 147 | +Key | Type | Description | Example |
| 148 | +------ | ------ | ------ | ------ |
| 149 | +`domains` | `{ default: string, [key: string]: string }` | **Required**, full URL to your domains | `domains: {default: 'http://myapi.tld', staging: 'http://staging.myapi.tld' },` |
| 150 | +`prefixes` | `{ default: string, [key: string]: string }` | **Required**, prefixes your API uses, `default` is required, leave it blank if you don't have any | `{ default: '/api/v1', apiV2: '/api/v2' }` |
| 151 | +`middlewares` | `APIMiddleware[]` | Optionnal middlewares, see [middlewares](#middlewares) | `[authFunc, trackFunc]` |
| 152 | +`debugAPI` | `boolean` | Optional, enables debugging mode, printing what's the wrapper doing |
| 153 | +`printNetworkRequests` | `boolean` | Optional, prints all your network requests |
| 154 | +`disableCache` | `boolean` | Optional, completely disables caching (overriden by service definitions & `fetch`'s `option` parameter) |
| 155 | +`cacheExpiration` | `number` | Optional default expiration of cached data in ms (overriden by service definitions & `fetch`'s `option` parameter) |
| 156 | +`offlineDriver` | `IAPIDriver` | Optional, see [use your own driver for caching](#use-your-own-driver-for-caching) |
| 157 | + |
| 158 | +## Services options |
| 159 | + |
| 160 | +These are the options for each of your services, **the only required key is `path`**. Default values are supplied for the others. |
| 161 | + |
| 162 | +Key | Type | Description | Example |
| 163 | +------ | ------ | ------ | ------ |
| 164 | +`path` | `string` | Required path to your endpoint, see [path and query parameters](#path-and-query-parameters) | `article/:articleId` |
| 165 | +`expiration` | `number` | Optional time in ms before this service's cached data becomes stale, defaults to 5 minutes |
| 166 | +`method` | `'GET' | 'POST' | 'OPTIONS'...` | Optional HTTP method of your request, defaults to `GET` |
| 167 | +`domain` | `string` | Optional specific domain to use for this service, provide the key you set in your `domains` API option |
| 168 | +`prefix` | `string` | Optional specific prefix to use for this service, provide the key you set in your `prefixes` API option |
| 169 | +`middlewares` | `APIMiddleware[]` | Optional array of middlewares that override the ones set globally in your `middlewares` API option, , see [middlewares](#middlewares) |
| 170 | +`disableCache` | `boolean` | Optional, disables the cache for this service (override your [API's global options](#api-options)) |
| 171 | + |
| 172 | +## Fetch options |
| 173 | + |
| 174 | +The `options` parameter of the `fetch` and `fetchHeaders` method overrides the configuration you set globally in your API options, and the one you set for your services definitions. For instance, this is a good way of making very specific calls without having to declare another service just to tweak a single option. |
| 175 | + |
| 176 | +Important notes : |
| 177 | + |
| 178 | +* All of these are optional. |
| 179 | +* All the keys of [services options](#services-options) can be overriden here ! You could disable caching just for a single call for example, but still having it enabled in your service's definition. |
| 180 | + |
| 181 | +Key | Type | Description | Example |
| 182 | +------ | ------ | ------ | ------ |
| 183 | +`pathParameters` | `{ [key: string]: string }` | Parameters to replace in your path, see [path and query parameters](#path-and-query-parameters) | `{ documentId: 'xSfdk21' }` |
| 184 | +`queryParameters` | `{ [key: string]: string }` | Query parameters that will be appended to your service's path, , see [path and query parameters](#path-and-query-parameters) | `{ refresh: true, orderBy: 'date' }` |
| 185 | +`headers` | `{ [key: string]: string }` | HTTP headers you need to pass in your request |
| 186 | +`middlewares` | `APIMiddleware[]` | Optional array of middlewares that override the ones set globally in your `middlewares` API option and in your service's definition, , see [middlewares](#middlewares) |
| 187 | +`fetchOptions` | `any` | Optional, any value passed here will be merged into the options of react-native's `fetch` method so you'll be able to configure anything not provided by the wrapper itself |
| 188 | + |
| 189 | +## Path and query parameters |
| 190 | + |
| 191 | +The URL to your endpoints are being constructed with **your domain name, your optional prefix, and your optional `pathParameters` and `queryParameters`.** |
| 192 | + |
| 193 | +* The `pathParameters` will replace the parameters in your service's path. For instance, a request fired with this path : `/documents/:documentId`, and these `pathParameters` : `{ documentId: 'xSfdk21' }` will become `/documents/xSfdk21`. |
| 194 | + |
| 195 | +* The `queryParameters` are regular query string parameters. For instance, a request fired with this path : `/weather` and these `queryParameters` : `{ days: 'mon,tue,sun', location: 'Paris,France' }` will become `/weather?days=mon,tue,sun&location=Paris,France`. |
| 196 | + |
| 197 | +## Middlewares |
| 198 | + |
| 199 | +Just like for the other request options, **you can provide middlewares at the global level in your API options, at the service's definition level, or in the `options` parameter of the `fetch` method.** |
| 200 | + |
| 201 | +You must provide an **array of promises**, like so : `(serviceDefinition: IAPIService, options: IFetchOptions) => any;`, please [take a look at the types](#types) to know more. You don't necessarily need to write asynchronous code in them, but they all must be promises. |
| 202 | + |
| 203 | +Anything you will resolve in those promises will be merged into your request's options ! |
| 204 | + |
| 205 | +Here's a barebone example : |
| 206 | + |
| 207 | +```javascript |
| 208 | +const API_OPTIONS = { |
| 209 | + // ... all your api options |
| 210 | + middlewares: [exampleMiddleware], |
| 211 | +}; |
| 212 | + |
| 213 | +async function exampleMiddleware (serviceDefinition, serviceOptions) { |
| 214 | + // This will be printed everytime you call a service |
| 215 | + console.log('You just fired a request for the path ' + serviceDefinition.path); |
| 216 | +} |
| 217 | +``` |
| 218 | + |
| 219 | +You can even make API calls in your middlewares. For instance, you might want to make sure the user is logged in into your API, or you might want to refresh its authentication token once in a while. Like so : |
| 220 | + |
| 221 | +```javascript |
| 222 | +const API_OPTIONS = { |
| 223 | + // ... all your api options |
| 224 | + middlewares: [authMiddleware] |
| 225 | +} |
| 226 | + |
| 227 | +async function authMiddleware (serviceDefinition, serviceOptions) { |
| 228 | + if (authToken && !tokenExpired) { |
| 229 | + // Our token is up-to-date, add it to the headers of our request |
| 230 | + return { headers: { 'X-Auth-Token': authToken } }; |
| 231 | + } |
| 232 | + // Token is missing or outdated, let's fetch a new one |
| 233 | + try { |
| 234 | + // Assuming our login service's method is already set to 'POST' |
| 235 | + const authData = await api.fetch( |
| 236 | + 'login', |
| 237 | + // the 'fetcthOptions' key allows us to use any of react-native's fetch method options |
| 238 | + // here, the body of our post request |
| 239 | + { fetchOptions: { body: 'username=user&password=password' } } |
| 240 | + ); |
| 241 | + // Store our new authentication token and add it to the headers of our request |
| 242 | + authToken = authData.authToken; |
| 243 | + tokenExpired = false; |
| 244 | + return { headers: { 'X-Auth-Token': authData.authToken } }; |
| 245 | + } catch (err) { |
| 246 | + throw new Error(`Couldn't auth to API, ${err}`); |
| 247 | + } |
| 248 | +} |
| 249 | +``` |
| 250 | + |
| 251 | +## Using your own driver for caching |
| 252 | + |
| 253 | +This wrapper has been written with the goal of **being storage-agnostic**. This means that by default, it will make use of react-native's `AsyncStorage` API, but feel free to write your own driver and use anything you want, like the amazing [realm](https://github.com/realm/realm-js) or [sqlite](https://github.com/andpor/react-native-sqlite-storage). |
| 254 | + |
| 255 | +> This is the first step for the wrapper to being also available on the browser and in any node.js environment. |
| 256 | +
|
| 257 | +Your custom driver must implement these 3 methods that are promises. |
| 258 | + |
| 259 | +* `getItem(key: string, callback?: (error?: Error, result?: string) => void)` |
| 260 | +* `setItem(key: string, value: string, callback?: (error?: Error) => void);` |
| 261 | +* `multiRemove(keys: string[], callback?: (errors?: Error[]) => void);` |
| 262 | + |
| 263 | +*Please note that, as of the 1.0 release, this hasn't been tested thoroughly.* |
| 264 | + |
| 265 | +## Types |
| 266 | + |
| 267 | +Every API interfaces [can be seen here](src/interfaces.ts) so you don't need to poke around the parameters in your console to be aware of what's available to you :) |
| 268 | + |
| 269 | +These are Typescript defintions, so they should be displayed in your editor/IDE if it supports it. |
| 270 | + |
| 271 | +## Roadmap |
| 272 | + |
| 273 | +Pull requests are more than welcome for these items, or for any feature that might be missing. |
| 274 | + |
| 275 | +- [ ] Write a demo |
| 276 | +- [ ] Thoroughly test custom caching drivers, maybe provide one (realm or sqlite) |
| 277 | +- [ ] Add automated testing |
0 commit comments