1 | [![SWR](https://assets.vercel.com/image/upload/v1572289618/swr/banner.png)](https://swr.vercel.app)
|
2 |
|
3 | <p align="center">
|
4 |
|
5 | <a aria-label="Vercel logo" href="https://vercel.com">
|
6 | <img src="https://badgen.net/badge/icon/Made%20by%20Vercel?icon=zeit&label&color=black&labelColor=black">
|
7 | </a>
|
8 | <br/>
|
9 |
|
10 | <a aria-label="NPM version" href="https://www.npmjs.com/package/swr">
|
11 | <img alt="" src="https://badgen.net/npm/v/swr">
|
12 | </a>
|
13 | <a aria-label="Package size" href="https://bundlephobia.com/result?p=swr">
|
14 | <img alt="" src="https://badgen.net/bundlephobia/minzip/swr">
|
15 | </a>
|
16 | <a aria-label="License" href="https://github.com/zeit/swr/blob/master/LICENSE">
|
17 | <img alt="" src="https://badgen.net/npm/license/swr">
|
18 | </a>
|
19 | </p>
|
20 |
|
21 | ## Introduction
|
22 |
|
23 | [swr.vercel.app](https://swr.vercel.app)
|
24 |
|
25 | SWR is a React Hooks library for remote data fetching.
|
26 |
|
27 | The name “**SWR**” is derived from `stale-while-revalidate`, a cache invalidation strategy popularized by [HTTP RFC 5861](https://tools.ietf.org/html/rfc5861).
|
28 | **SWR** first returns the data from cache (stale), then sends the fetch request (revalidate), and finally comes with the up-to-date data again.
|
29 |
|
30 | It features:
|
31 |
|
32 | - Transport and protocol agnostic data fetching
|
33 | - Fast page navigation
|
34 | - Revalidation on focus
|
35 | - Interval polling
|
36 | - Request deduplication
|
37 | - Local mutation
|
38 | - Pagination
|
39 | - TypeScript ready
|
40 | - SSR support
|
41 | - Suspense mode
|
42 | - React Native support
|
43 | - Minimal API
|
44 |
|
45 | ...and a lot more.
|
46 |
|
47 | With SWR, components will get **a stream of data updates constantly and automatically**. Thus, the UI will be always **fast** and **reactive**.
|
48 |
|
49 | <br/>
|
50 |
|
51 | ## Quick Start
|
52 |
|
53 | ```js
|
54 | import useSWR from 'swr'
|
55 |
|
56 | function Profile() {
|
57 | const { data, error } = useSWR('/api/user', fetcher)
|
58 |
|
59 | if (error) return <div>failed to load</div>
|
60 | if (!data) return <div>loading...</div>
|
61 | return <div>hello {data.name}!</div>
|
62 | }
|
63 | ```
|
64 |
|
65 | In this example, the React Hook `useSWR` accepts a `key` and a `fetcher` function.
|
66 | The `key` is a unique identifier of the request, normally the URL of the API. And the `fetcher` accepts
|
67 | `key` as its parameter and returns the data asynchronously.
|
68 |
|
69 | `useSWR` also returns 2 values: `data` and `error`. When the request (fetcher) is not yet finished,
|
70 | `data` will be `undefined`. And when we get a response, it sets `data` and `error` based on the result
|
71 | of `fetcher` and rerenders the component.
|
72 |
|
73 | Note that `fetcher` can be any asynchronous function, so you can use your favourite data-fetching
|
74 | library to handle that part.
|
75 |
|
76 | Check out [swr.vercel.app](https://swr.vercel.app) for more demos of SWR, and [Examples](#examples) for the best practices.
|
77 |
|
78 | <br/>
|
79 |
|
80 | ## Usage
|
81 |
|
82 | Inside your React project directory, run the following:
|
83 |
|
84 | ```
|
85 | yarn add swr
|
86 | ```
|
87 |
|
88 | Or with npm:
|
89 |
|
90 | ```
|
91 | npm install swr
|
92 | ```
|
93 |
|
94 | ### API
|
95 |
|
96 | ```js
|
97 | const { data, error, isValidating, mutate } = useSWR(key, fetcher, options)
|
98 | ```
|
99 |
|
100 | #### Parameters
|
101 |
|
102 | - `key`: a unique key string for the request (or a function / array / null) [(advanced usage)](#conditional-fetching)
|
103 | - `fetcher`: (_optional_) a Promise returning function to fetch your data [(details)](#data-fetching)
|
104 | - `options`: (_optional_) an object of options for this SWR hook
|
105 |
|
106 | #### Return Values
|
107 |
|
108 | - `data`: data for the given key resolved by `fetcher` (or undefined if not loaded)
|
109 | - `error`: error thrown by `fetcher` (or undefined)
|
110 | - `isValidating`: if there's a request or revalidation loading
|
111 | - `mutate(data?, shouldRevalidate?)`: function to mutate the cached data
|
112 |
|
113 | #### Options
|
114 |
|
115 | - `suspense = false`: enable React Suspense mode [(details)](#suspense-mode)
|
116 | - `fetcher = undefined`: the default fetcher function
|
117 | - `initialData`: initial data to be returned (note: This is per-hook)
|
118 | - `revalidateOnMount`: enable or disable automatic revalidation when component is mounted (by default revalidation occurs on mount when initialData is not set, use this flag to force behavior)
|
119 | - `revalidateOnFocus = true`: auto revalidate when window gets focused
|
120 | - `revalidateOnReconnect = true`: automatically revalidate when the browser regains a network connection (via `navigator.onLine`)
|
121 | - `refreshInterval = 0`: polling interval (disabled by default)
|
122 | - `refreshWhenHidden = false`: polling when the window is invisible (if `refreshInterval` is enabled)
|
123 | - `refreshWhenOffline = false`: polling when the browser is offline (determined by `navigator.onLine`)
|
124 | - `shouldRetryOnError = true`: retry when fetcher has an error [(details)](#error-retries)
|
125 | - `dedupingInterval = 2000`: dedupe requests with the same key in this time span
|
126 | - `focusThrottleInterval = 5000`: only revalidate once during a time span
|
127 | - `loadingTimeout = 3000`: timeout to trigger the onLoadingSlow event
|
128 | - `errorRetryInterval = 5000`: error retry interval [(details)](#error-retries)
|
129 | - `errorRetryCount`: max error retry count [(details)](#error-retries)
|
130 | - `onLoadingSlow(key, config)`: callback function when a request takes too long to load (see `loadingTimeout`)
|
131 | - `onSuccess(data, key, config)`: callback function when a request finishes successfully
|
132 | - `onError(err, key, config)`: callback function when a request returns an error
|
133 | - `onErrorRetry(err, key, config, revalidate, revalidateOps)`: handler for [error retry](#error-retries)
|
134 | - `compare(a, b)`: comparison function used to detect when returned data has changed, to avoid spurious rerenders. By default, [`dequal/lite`](https://github.com/lukeed/dequal) is used.
|
135 |
|
136 | When under a slow network (2G, <= 70Kbps), `errorRetryInterval` will be 10s, and
|
137 | `loadingTimeout` will be 5s by default.
|
138 |
|
139 | You can also use a [global configuration](#global-configuration) to provide default options.
|
140 |
|
141 | <br/>
|
142 |
|
143 | ## Examples
|
144 |
|
145 | - [Global Configuration](#global-configuration)
|
146 | - [Data Fetching](#data-fetching)
|
147 | - [Conditional Fetching](#conditional-fetching)
|
148 | - [Dependent Fetching](#dependent-fetching)
|
149 | - [Multiple Arguments](#multiple-arguments)
|
150 | - [Manually Revalidate](#manually-revalidate)
|
151 | - [Mutation and Post Request](#mutation-and-post-request)
|
152 | - [Mutate Based on Current Data](#mutate-based-on-current-data)
|
153 | - [Returned Data from Mutate](#returned-data-from-mutate)
|
154 | - [SSR with Next.js](#ssr-with-nextjs)
|
155 | - [Suspense Mode](#suspense-mode)
|
156 | - [Error Retries](#error-retries)
|
157 | - [Prefetching Data](#prefetching-data)
|
158 | - [Request Deduplication](#request-deduplication)
|
159 |
|
160 | ### Global Configuration
|
161 |
|
162 | The context `SWRConfig` can provide global configurations (`options`) for all SWR hooks.
|
163 |
|
164 | In this example, all SWRs will use the same fetcher provided to load JSON data, and refresh every 3 seconds by default:
|
165 |
|
166 | ```js
|
167 | import useSWR, { SWRConfig } from 'swr'
|
168 |
|
169 | function Dashboard() {
|
170 | const { data: events } = useSWR('/api/events')
|
171 | const { data: projects } = useSWR('/api/projects')
|
172 | const { data: user } = useSWR('/api/user', { refreshInterval: 0 }) // don't refresh
|
173 | // ...
|
174 | }
|
175 |
|
176 | function App() {
|
177 | return (
|
178 | <SWRConfig
|
179 | value={{
|
180 | refreshInterval: 3000,
|
181 | fetcher: (...args) => fetch(...args).then(res => res.json())
|
182 | }}
|
183 | >
|
184 | <Dashboard />
|
185 | </SWRConfig>
|
186 | )
|
187 | }
|
188 | ```
|
189 |
|
190 | ### Data Fetching
|
191 |
|
192 | `fetcher` is a function that **accepts the `key`** of SWR, and returns a value or a Promise.
|
193 | You can use any library to handle data fetching, for example:
|
194 |
|
195 | ```js
|
196 | import fetch from 'unfetch'
|
197 |
|
198 | const fetcher = url => fetch(url).then(r => r.json())
|
199 |
|
200 | function App() {
|
201 | const { data } = useSWR('/api/data', fetcher)
|
202 | // ...
|
203 | }
|
204 | ```
|
205 |
|
206 | Or using GraphQL:
|
207 |
|
208 | ```js
|
209 | import { request } from 'graphql-request'
|
210 |
|
211 | const API = 'https://api.graph.cool/simple/v1/movies'
|
212 | const fetcher = query => request(API, query)
|
213 |
|
214 | function App() {
|
215 | const { data, error } = useSWR(
|
216 | `{
|
217 | Movie(title: "Inception") {
|
218 | releaseDate
|
219 | actors {
|
220 | name
|
221 | }
|
222 | }
|
223 | }`,
|
224 | fetcher
|
225 | )
|
226 | // ...
|
227 | }
|
228 | ```
|
229 |
|
230 | _If you want to pass variables to a GraphQL query, check out [Multiple Arguments](#multiple-arguments)._
|
231 |
|
232 | Note that `fetcher` can be omitted from the parameters if it's provided globally.
|
233 |
|
234 | ### Conditional Fetching
|
235 |
|
236 | Use `null` or pass a function as the `key` to `useSWR` to conditionally fetch data. If the functions throws an error or returns a falsy value, SWR will cancel the request.
|
237 |
|
238 | ```js
|
239 | // conditionally fetch
|
240 | const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
|
241 |
|
242 | // ...or return a falsy value
|
243 | const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)
|
244 |
|
245 | // ... or throw an error when user.id is not defined
|
246 | const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)
|
247 | ```
|
248 |
|
249 | ### Dependent Fetching
|
250 |
|
251 | SWR also allows you to fetch data that depends on other data. It ensures the maximum possible parallelism (avoiding waterfalls), as well as serial fetching when a piece of dynamic data is required for the next data fetch to happen.
|
252 |
|
253 | ```js
|
254 | function MyProjects() {
|
255 | const { data: user } = useSWR('/api/user')
|
256 | const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id)
|
257 | // When passing a function, SWR will use the return
|
258 | // value as `key`. If the function throws or returns
|
259 | // falsy, SWR will know that some dependencies are not
|
260 | // ready. In this case `user.id` throws when `user`
|
261 | // isn't loaded.
|
262 |
|
263 | if (!projects) return 'loading...'
|
264 | return 'You have ' + projects.length + ' projects'
|
265 | }
|
266 | ```
|
267 |
|
268 | ### Multiple Arguments
|
269 |
|
270 | In some scenarios, it's useful to pass multiple arguments (can be any value or object) to the `fetcher` function. For example:
|
271 |
|
272 | ```js
|
273 | useSWR('/api/user', url => fetchWithToken(url, token))
|
274 | ```
|
275 |
|
276 | This is **incorrect**. Because the identifier (also the index of the cache) of the data is `'/api/user'`,
|
277 | so even if `token` changes, SWR will still have the same key and return the wrong data.
|
278 |
|
279 | Instead, you can use an **array** as the `key` parameter, which contains multiple arguments of `fetcher`:
|
280 |
|
281 | ```js
|
282 | const { data: user } = useSWR(['/api/user', token], fetchWithToken)
|
283 |
|
284 | // ...and pass it as an argument to another query
|
285 | const { data: orders } = useSWR(user ? ['/api/orders', user] : null, fetchWithUser)
|
286 | ```
|
287 |
|
288 | The key of the request is now the combination of both values. SWR **shallowly** compares
|
289 | the arguments on every render and triggers revalidation if any of them has changed.
|
290 | Keep in mind that you should not recreate objects when rendering, as they will be treated as different objects on every render:
|
291 |
|
292 | ```js
|
293 | // Don’t do this! Deps will be changed on every render.
|
294 | useSWR(['/api/user', { id }], query)
|
295 |
|
296 | // Instead, you should only pass “stable” values.
|
297 | useSWR(['/api/user', id], (url, id) => query(url, { id }))
|
298 | ```
|
299 |
|
300 | Dan Abramov explains dependencies very well in [this blog post](https://overreacted.io/a-complete-guide-to-useeffect/#but-i-cant-put-this-function-inside-an-effect).
|
301 |
|
302 | ### Manually Revalidate
|
303 |
|
304 | You can broadcast a revalidation message globally to all SWRs with the same key by calling
|
305 | `mutate(key)`.
|
306 |
|
307 | This example shows how to automatically refetch the login info (e.g.: inside `<Profile/>`)
|
308 | when the user clicks the “Logout” button.
|
309 |
|
310 | ```js
|
311 | import useSWR, { mutate } from 'swr'
|
312 |
|
313 | function App() {
|
314 | return (
|
315 | <div>
|
316 | <Profile />
|
317 | <button onClick={() => {
|
318 | // set the cookie as expired
|
319 | document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
|
320 |
|
321 | // tell all SWRs with this key to revalidate
|
322 | mutate('/api/user')
|
323 | }}>
|
324 | Logout
|
325 | </button>
|
326 | </div>
|
327 | )
|
328 | }
|
329 | ```
|
330 |
|
331 | ### Mutation and Post Request
|
332 |
|
333 | In many cases, applying local mutations to data is a good way to make changes
|
334 | feel faster — no need to wait for the remote source of data.
|
335 |
|
336 | With `mutate`, you can update your local data programmatically, while
|
337 | revalidating and finally replace it with the latest data.
|
338 |
|
339 | ```js
|
340 | import useSWR, { mutate } from 'swr'
|
341 |
|
342 | function Profile() {
|
343 | const { data } = useSWR('/api/user', fetcher)
|
344 |
|
345 | return (
|
346 | <div>
|
347 | <h1>My name is {data.name}.</h1>
|
348 | <button onClick={async () => {
|
349 | const newName = data.name.toUpperCase()
|
350 | // send a request to the API to update the data
|
351 | await requestUpdateUsername(newName)
|
352 | // update the local data immediately and revalidate (refetch)
|
353 | // NOTE: key has to be passed to mutate as it's not bound
|
354 | mutate('/api/user', { ...data, name: newName })
|
355 | }}>Uppercase my name!</button>
|
356 | </div>
|
357 | )
|
358 | }
|
359 | ```
|
360 |
|
361 | Clicking the button in the example above will send a POST request to modify the remote data, locally update the client data and
|
362 | try to fetch the latest one (revalidate).
|
363 |
|
364 | But many POST APIs will just return the updated data directly, so we don’t need to revalidate again.
|
365 | Here’s an example showing the “local mutate - request - update” usage:
|
366 |
|
367 | ```js
|
368 | mutate('/api/user', newUser, false) // use `false` to mutate without revalidation
|
369 | mutate('/api/user', updateUser(newUser)) // `updateUser` is a Promise of the request,
|
370 | // which returns the updated document
|
371 | ```
|
372 |
|
373 | ### Mutate Based on Current Data
|
374 |
|
375 | In many cases, you are receiving a single value back from your API and want to update a list of them.
|
376 |
|
377 | With `mutate`, you can pass an async function which will receive the current cached value, if any, and let you return an updated document.
|
378 |
|
379 | ```js
|
380 | mutate('/api/users', async users => {
|
381 | const user = await fetcher('/api/users/1')
|
382 | return [user, ...users.slice(1)]
|
383 | })
|
384 | ```
|
385 |
|
386 | ### Returned Data from Mutate
|
387 |
|
388 | Most probably, you need some data to update the cache. The data is resolved or returned from the promise or async function you passed to `mutate`.
|
389 |
|
390 | The function will return an updated document to let `mutate` update the corresponding cache value. It could throw an error somehow, every time when you call it.
|
391 |
|
392 | ```js
|
393 | try {
|
394 | const user = await mutate('/api/user', updateUser(newUser))
|
395 | } catch (error) {
|
396 | // Handle an error while updating the user here
|
397 | }
|
398 | ```
|
399 |
|
400 | ### Bound `mutate()`
|
401 |
|
402 | The SWR object returned by `useSWR` also contains a `mutate()` function that is pre-bound to the SWR's key.
|
403 |
|
404 | It is functionally equivalent to the global `mutate` function but does not require the `key` parameter.
|
405 |
|
406 | ```js
|
407 | import useSWR from 'swr'
|
408 |
|
409 | function Profile() {
|
410 | const { data, mutate } = useSWR('/api/user', fetcher)
|
411 |
|
412 | return (
|
413 | <div>
|
414 | <h1>My name is {data.name}.</h1>
|
415 | <button onClick={async () => {
|
416 | const newName = data.name.toUpperCase()
|
417 | // send a request to the API to update the data
|
418 | await requestUpdateUsername(newName)
|
419 | // update the local data immediately and revalidate (refetch)
|
420 | // NOTE: key is not required when using useSWR's mutate as it's pre-bound
|
421 | mutate({ ...data, name: newName })
|
422 | }}>Uppercase my name!</button>
|
423 | </div>
|
424 | )
|
425 | }
|
426 | ```
|
427 |
|
428 | ### SSR with Next.js
|
429 |
|
430 | With the `initialData` option, you pass an initial value to the hook. It works perfectly with many SSR solutions
|
431 | such as `getServerSideProps` in [Next.js](https://github.com/zeit/next.js):
|
432 |
|
433 | ```js
|
434 | export async function getServerSideProps() {
|
435 | const data = await fetcher('/api/data')
|
436 | return { props: { data } }
|
437 | }
|
438 |
|
439 | function App(props) {
|
440 | const initialData = props.data
|
441 | const { data } = useSWR('/api/data', fetcher, { initialData })
|
442 |
|
443 | return <div>{data}</div>
|
444 | }
|
445 | ```
|
446 |
|
447 | It is still a server-side rendered site, but it’s also fully powered by SWR in the client-side.
|
448 | Which means the data can be dynamic and update itself over time and user interactions.
|
449 |
|
450 | ### Suspense Mode
|
451 |
|
452 | You can enable the `suspense` option to use SWR with React Suspense:
|
453 |
|
454 | ```js
|
455 | import { Suspense } from 'react'
|
456 | import useSWR from 'swr'
|
457 |
|
458 | function Profile() {
|
459 | const { data } = useSWR('/api/user', fetcher, { suspense: true })
|
460 | return <div>hello, {data.name}</div>
|
461 | }
|
462 |
|
463 | function App() {
|
464 | return (
|
465 | <Suspense fallback={<div>loading...</div>}>
|
466 | <Profile/>
|
467 | </Suspense>
|
468 | )
|
469 | }
|
470 | ```
|
471 |
|
472 | In Suspense mode, `data` is always the fetch response (so you don't need to check if it's `undefined`).
|
473 | But if an error occurred, you need to use an [error boundary](https://reactjs.org/docs/concurrent-mode-suspense.html#handling-errors) to catch it.
|
474 |
|
475 | _Note that Suspense is not supported in SSR mode._
|
476 |
|
477 | ### Error Retries
|
478 |
|
479 | By default, SWR uses the [exponential backoff algorithm](https://en.wikipedia.org/wiki/Exponential_backoff) to handle error retries.
|
480 | You can read more from the source code.
|
481 |
|
482 | It's also possible to override the behavior:
|
483 |
|
484 | ```js
|
485 | useSWR(key, fetcher, {
|
486 | onErrorRetry: (error, key, option, revalidate, { retryCount }) => {
|
487 | if (retryCount >= 10) return
|
488 | if (error.status === 404) return
|
489 |
|
490 | // retry after 5 seconds
|
491 | setTimeout(() => revalidate({ retryCount: retryCount + 1 }), 5000)
|
492 | }
|
493 | })
|
494 | ```
|
495 |
|
496 | ### Prefetching Data
|
497 |
|
498 | There’re many ways to prefetch the data for SWR. For top-level requests, [`rel="preload"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content) is highly recommended:
|
499 |
|
500 | ```html
|
501 | <link rel="preload" href="/api/data" as="fetch" crossorigin="anonymous">
|
502 | ```
|
503 |
|
504 | This will prefetch the data before the JavaScript starts downloading. And your incoming fetch requests will reuse the result (including SWR, of course).
|
505 |
|
506 | Another choice is to prefetch the data conditionally. You can have a function to refetch and set the cache:
|
507 |
|
508 | ```js
|
509 | function prefetch() {
|
510 | mutate('/api/data', fetch('/api/data').then(res => res.json()))
|
511 | // the second parameter is a Promise
|
512 | // SWR will use the result when it resolves
|
513 | }
|
514 | ```
|
515 |
|
516 | And use it when you need to preload the **resources** (for example when [hovering](https://github.com/GoogleChromeLabs/quicklink) [a](https://github.com/guess-js/guess) [link](https://instant.page)).
|
517 | Together with techniques like [page prefetching](https://nextjs.org/docs#prefetching-pages) in Next.js, you will be able to load both next page and data instantly.
|
518 |
|
519 | ### Request Deduplication
|
520 |
|
521 | SWR deduplicates requests by default. If you call the hook with the same key multiple times, only one request is made. Duplicated calls will receive a value from cache.
|
522 | Here, the 'api/user' key is used in two requests:
|
523 |
|
524 | ```js
|
525 | import useSWR from 'swr'
|
526 |
|
527 | function UserProfileName() {
|
528 | const { data, error } = useSWR('/api/user', fetcher)
|
529 |
|
530 | if (error) return <div>failed to load</div>
|
531 | if (!data) return <div>loading...</div>
|
532 | return <p>Name: {data.name}!</p>
|
533 | }
|
534 |
|
535 | function UserProfileAvatar() {
|
536 | const { data, error } = useSWR('/api/user', fetcher)
|
537 |
|
538 | if (error) return <div>failed to load</div>
|
539 | if (!data) return <div>loading...</div>
|
540 | return <img src={data.avatarUrl} alt="Profile image" />
|
541 | }
|
542 |
|
543 | export default function App() {
|
544 | return (
|
545 | <div>
|
546 | <UserProfileName />
|
547 | <UserProfileAvatar />
|
548 | </div>
|
549 | )
|
550 | }
|
551 | ```
|
552 |
|
553 | By default, requests made within 2 seconds are deduped. This can be changed by setting the `dedupingInterval` option:
|
554 |
|
555 | ```js
|
556 | const { data, error } = useSWR('/api/user', fetcher, { dedupingInterval: 1000 })
|
557 | ```
|
558 |
|
559 | This will deduplicate requests at an interval of 1 second.
|
560 | <br/>
|
561 |
|
562 | ## Authors
|
563 |
|
564 | - Shu Ding ([@shuding_](https://twitter.com/shuding_)) – [Vercel](https://vercel.com)
|
565 | - Guillermo Rauch ([@rauchg](https://twitter.com/rauchg)) – [Vercel](https://vercel.com)
|
566 | - Joe Haddad ([@timer150](https://twitter.com/timer150)) - [Vercel](https://vercel.com)
|
567 | - Paco Coursey ([@pacocoursey](https://twitter.com/pacocoursey)) - [Vercel](https://vercel.com)
|
568 |
|
569 | Thanks to Ryan Chen for providing the awesome `swr` npm package name!
|
570 |
|
571 | <br/>
|
572 |
|
573 | ## License
|
574 |
|
575 | The MIT License.
|