├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── cat-fact │ ├── App.res │ └── index.html ├── error.res ├── fetch.res ├── middleware.res └── provider.res ├── package.json ├── pnpm-lock.yaml ├── prepublish.sh ├── rescript.json └── src ├── Swr.res ├── SwrCommon.res ├── SwrEasy.res ├── SwrInfinite.res └── SwrMutation.res /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | .merlin 4 | .bsb.lock 5 | npm-debug.log 6 | /lib/bs/ 7 | /lib/ocaml/ 8 | /node_modules/ 9 | debug.log 10 | *.cmi 11 | *.cmj 12 | *.cmt 13 | *.bs.js 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | .merlin 4 | .bsb.lock 5 | npm-debug.log 6 | /lib/bs/ 7 | /lib/ocaml/ 8 | /node_modules/ 9 | debug.log 10 | *.cmi 11 | *.cmj 12 | *.cmt 13 | *.bs.js 14 | /examples/ 15 | 16 | prepublish.sh 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. 4 | 5 | ## 3.0.0-beta.1 (2022-09-10) 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Roddy MacSween 4 | Copyright (c) 2021 Tamim Arafat 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rescript-swr 2 | [![npm](https://img.shields.io/npm/v/rescript-swr)](https://www.npmjs.com/package/rescript-swr) 3 | ![npm](https://img.shields.io/npm/dm/rescript-swr?color=blue) 4 | 5 | Low-overhead [ReScript](https://rescript-lang.org) bindings to [SWR](https://github.com/vercel/swr). 6 | Supports version >=2.0.0 & <=3.0.0. Compatible with ReScript v11 and onwards. 7 | 8 | Includes experimental module `SwrEasy` that has a layer of abstraction 9 | designed for a smoother developer experience, with the tradeoff being a slightly higher runtime cost. 10 | 11 | ## Installation 12 | Run 13 | ``` 14 | npm install rescript-swr swr 15 | ``` 16 | then update the `bs-dependencies` key in your `rescript.json` file to include "`rescript-swr`". 17 | 18 | ## Examples 19 | ```rescript 20 | @react.component 21 | let make = () => { 22 | let config = { 23 | refreshInterval: 10000, 24 | loadingTimeout: 3000, 25 | fallbackData: {cats: 0, dogs: 0, hamsters: 0}, 26 | onLoadingSlow: (_, _) => { 27 | Console.log("This is taking too long") 28 | }, 29 | onErrorRetry: (_error, _key, _config, revalidate, {?retryCount}) => { 30 | // Retry after 5 seconds. 31 | setTimeout( 32 | () => revalidate({?retryCount})->ignore, 33 | 5000, 34 | )->ignore 35 | }, 36 | use: [ 37 | // logger middleware 38 | (useSWRNext) => (key, fetcher, config) => { 39 | let extendedFetcher = args => { 40 | Console.log2("SWR Request: ", key) 41 | fetcher(args) 42 | } 43 | useSWRNext(key, extendedFetcher, config) 44 | }, 45 | ], 46 | } 47 | 48 | let {data, error} = Swr.useSWR_config( 49 | "/api/user1/pets", 50 | fetcher, 51 | config 52 | ); 53 | 54 | switch (data) { 55 | | Some(data) => render(data) 56 | | None => render_loading() 57 | }; 58 | }; 59 | ``` 60 | ### Provide global configuration 61 | ```rescript 62 | { 64 | fallback: Obj.magic({ 65 | "/api/user1/pets": { 66 | "dogs": 2, 67 | "cats": 3, 68 | "hamsters": 1, 69 | }, 70 | "/api/user2/pets": { 71 | "dogs": 1, 72 | "cats": 2, 73 | "hamsters": 0, 74 | }, 75 | }) 76 | }}> 77 | 78 | 79 | ``` 80 | ### Obtain global configuration 81 | ```rescript 82 | open Swr 83 | 84 | // return all global configurations 85 | let globalConfig = SwrConfiguration.useSWRConfig() 86 | // access configuration property using auto-generated getter 87 | let refreshInterval = globalConfig.refreshInterval 88 | Js.log(refreshInterval) 89 | 90 | // broadcast a revalidation message globally to other SWR hooks 91 | globalConfig->SwrConfiguration.mutateKey(#Key("/api/user1/pets")) 92 | 93 | // update the local data immediately, but disable the revalidation 94 | globalConfig->SwrConfiguration.mutateWithOpts(#Key("/api/user1/pets"), _ => Js.Promise.resolve({dogs: 2, hamsters: 5, cats: 10})->Some, { revalidate: false }) 95 | ``` 96 | 97 | ## Documentation 98 | See [DOCUMENTATION.md](https://github.com/arafatamim/rescript-swr/blob/main/DOCUMENTATION.md). 99 | 100 | ## Credits 101 | Originally forked from https://github.com/roddyyaga/bs-swr. 102 | 103 | ## License 104 | MIT Licensed. See [LICENSE](https://github.com/arafatamim/rescript-swr/blob/main/LICENSE) file. 105 | 106 | -------------------------------------------------------------------------------- /examples/cat-fact/App.res: -------------------------------------------------------------------------------- 1 | type response = {fact: string} 2 | 3 | @val 4 | external fetch: string => promise<{"json": unit => promise}> = "fetch" 5 | 6 | let sleeper = (ms, x) => Promise.make((resolve, _) => setTimeout(_ => resolve(x), ms)->ignore) 7 | 8 | let fetcher = key => { 9 | fetch("https://catfact.ninja" ++ key) 10 | ->Promise.then(res => res["json"]()) 11 | ->Promise.thenResolve(res => res.fact) 12 | } 13 | 14 | let renderData = data => ("Cat fact: " ++ data)->React.string 15 | let renderError = (err: SwrEasy.error) =>

{("Error: " ++ err.message)->React.string}

16 | 17 | module App = { 18 | @react.component 19 | let make = () => { 20 | open React 21 | open SwrEasy 22 | 23 | let {result, mutate} = useSWR("/fact", fetcher) 24 | 25 | let btns = 26 | <> 27 | 28 | 29 | 39 | 40 | 41 | let render = switch result { 42 | | Pending =>

{"Loading..."->string}

43 | | Refresh(Ok(data)) => 44 | <> 45 |

{renderData(data)}

46 |

{"Revalidating..."->string}

47 | 48 | | Refresh(Error(err)) => 49 | <> 50 |

{renderError(err)}

51 |

{"Validating..."->string}

52 | 53 | | Replete(Ok(data)) => 54 | <> 55 |

{renderData(data)}

56 |

{btns}

57 | 58 | | Replete(Error(err)) => 59 | <> 60 |

{renderError(err)}

61 |

{btns}

62 | 63 | | _ => 64 | <> 65 |

{"Cat fact not loaded"->string}

66 |

67 | 68 |

69 | 70 | } 71 | 72 |
73 | {render} 74 |

75 | {"Click on "->string} 76 | {"this"->string} 77 | {" link to see the JavaScript equivalent of this code."->string} 78 |

79 |
80 | } 81 | } 82 | 83 | switch ReactDOM.querySelector("#root") { 84 | | Some(domNode) => { 85 | open ReactDOM.Client 86 | createRoot(domNode)->Root.render() 87 | } 88 | | None => "Mount root missing!"->Console.error 89 | } 90 | -------------------------------------------------------------------------------- /examples/cat-fact/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Cat Fact 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/error.res: -------------------------------------------------------------------------------- 1 | // Examples in this file have been adapted 2 | // from https://swr.vercel.app/docs/error-handling 3 | open Swr 4 | open Fetch 5 | 6 | type state<'a> = Idle | Loading | Resolved('a) 7 | 8 | @react.component 9 | let make = (~url) => { 10 | let {data, error} = useSWR_config( 11 | url, 12 | fetcher, 13 | { 14 | onErrorRetry: (error, key, _config, revalidate, opts) => { 15 | Console.log(error) 16 | switch key { 17 | | "/api/user" => () 18 | | _ => setTimeout(() => revalidate(opts)->ignore, 5000)->ignore 19 | } 20 | }, 21 | }, 22 | ) 23 | let state = switch (data, error) { 24 | | (None, None) => Loading 25 | | (Some(data), None) => Resolved(Ok(data)) 26 | | (None, Some(err)) => Resolved(Error(err)) 27 | | (_, _) => Idle 28 | } 29 | 30 |
31 | {switch state { 32 | | Loading => "Loading..." 33 | | Resolved(Ok(data)) => "Got data: " ++ data 34 | | Resolved(Error(error)) => 35 | "Error: " ++ Exn.message(error)->Option.getOr("Unknown exception!") 36 | | Idle => "Not doing anything!" 37 | }->React.string} 38 |
39 | } 40 | -------------------------------------------------------------------------------- /examples/fetch.res: -------------------------------------------------------------------------------- 1 | exception FetchError(string, int) 2 | 3 | let fetcher = (url) => { 4 | Promise.make((resolve, reject)=>{ 5 | if (url === "") { 6 | reject(FetchError("Url is empty!", 400)) 7 | } 8 | else { 9 | resolve("Got data!") 10 | } 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /examples/middleware.res: -------------------------------------------------------------------------------- 1 | /* 2 | Examples adapted from https://swr.vercel.app/docs/middleware 3 | */ 4 | 5 | open Swr 6 | 7 | let logger = useSWRNext => (key, fetcher, config) => { 8 | let extendedFetcher = args => { 9 | Console.log2("SWR Request: ", key) 10 | fetcher(args) 11 | } 12 | useSWRNext(key, extendedFetcher, config) 13 | } 14 | 15 | let swr = useSWR_config( 16 | "key", 17 | Fetch.fetcher, 18 | { 19 | use: [logger], 20 | }, 21 | ) 22 | Console.log(swr.data) 23 | -------------------------------------------------------------------------------- /examples/provider.res: -------------------------------------------------------------------------------- 1 | open Swr 2 | 3 | /* bindings */ 4 | // JSON.parse 5 | @scope("JSON") @val 6 | external parseArray: string => array<'t> = "parse" 7 | // JSON.stringify 8 | @scope("JSON") @val 9 | external stringifyArray: array<'t> => string = "stringify" 10 | // window.addEventListener 11 | @val @scope("window") 12 | external addEventListener: (string, 'event => unit) => unit = "addEventListener" 13 | 14 | let setupCache = map => { 15 | { 16 | get: key => { 17 | map->Map.get(key)->Option.map(data => {data}) 18 | }, 19 | set: (key, value) => { 20 | switch value.data { 21 | | Some(data) => map->Map.set(key, {data: data}) 22 | | _ => () 23 | } 24 | }, 25 | delete: key => { 26 | map->Map.delete(key)->ignore 27 | }, 28 | keys: () => { 29 | map->Map.keys 30 | }, 31 | } 32 | } 33 | 34 | /* 35 | Example stolen from 36 | https://swr.vercel.app/docs/advanced/cache#localstorage-based-persistent-cache 37 | */ 38 | let localStorageProvider: unit => cache = () => { 39 | open Dom.Storage2 40 | 41 | 42 | // When initializing, we restore the data from `localStorage` into a map. 43 | let cache = localStorage->getItem("app-cache")->Option.getOr("[]")->parseArray 44 | let map = Map.fromArray(cache) 45 | 46 | // Before unloading the app, we write back all the data into `localStorage`. 47 | addEventListener("beforeunload", () => { 48 | let appCache = stringifyArray(map->Map.entries->Array.fromIterator) 49 | localStorage->setItem("app-cache", appCache) 50 | }) 51 | 52 | setupCache(map) 53 | } 54 | 55 | module LocalStorageProvider = { 56 | @react.component 57 | let make = () => { 58 | { 60 | dedupingInterval: ?config.dedupingInterval->Option.map(v => v * 5), 61 | revalidateOnFocus: false, 62 | suspense: true, 63 | provider: localStorageProvider(), 64 | }}> 65 |
66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rescript-swr", 3 | "description": "SWR bindings for ReScript", 4 | "version": "3.0.0-beta.4", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/arafatamim/rescript-swr.git" 8 | }, 9 | "scripts": { 10 | "prepublishOnly": "sh prepublish.sh", 11 | "prepare": "rescript clean -with-deps && rescript build", 12 | "build": "rescript build", 13 | "start": "rescript build -w", 14 | "clean": "rescript clean" 15 | }, 16 | "keywords": [ 17 | "bucklescript", 18 | "rescript", 19 | "react", 20 | "swr" 21 | ], 22 | "license": "MIT", 23 | "author": { 24 | "name": "Tamim Arafat", 25 | "email": "tamim.arafat@gmail.com" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/arafatamim/rescript-swr/issues" 29 | }, 30 | "homepage": "https://github.com/arafatamim/rescript-swr#readme", 31 | "devDependencies": { 32 | "react-dom": "^18.3.1", 33 | "rescript": "^11.1.3" 34 | }, 35 | "peerDependencies": { 36 | "@rescript/react": "^0.10.3", 37 | "swr": "^2.0.0" 38 | }, 39 | "dependencies": { 40 | "@rescript/core": "^1.5.2", 41 | "@rescript/react": "0.13.0", 42 | "react": "^18.3.1", 43 | "swr": "2.2.5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@rescript/core': 12 | specifier: ^1.5.2 13 | version: 1.5.2(rescript@11.1.3) 14 | '@rescript/react': 15 | specifier: 0.13.0 16 | version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 17 | react: 18 | specifier: ^18.3.1 19 | version: 18.3.1 20 | swr: 21 | specifier: 2.2.5 22 | version: 2.2.5(react@18.3.1) 23 | devDependencies: 24 | react-dom: 25 | specifier: ^18.3.1 26 | version: 18.3.1(react@18.3.1) 27 | rescript: 28 | specifier: ^11.1.3 29 | version: 11.1.3 30 | 31 | packages: 32 | 33 | '@rescript/core@1.5.2': 34 | resolution: {integrity: sha512-VWRFHrQu8hWnd9Y9LYZ8kig2urybhZlDVGy5u50bqf2WCRAeysBIfxK8eN4VlpQT38igMo0/uLX1KSpwCVMYGw==} 35 | peerDependencies: 36 | rescript: ^11.1.0-rc.7 37 | 38 | '@rescript/react@0.13.0': 39 | resolution: {integrity: sha512-YSIWIyMlyF9ZaP6Q3hScl1h3wRbdIP4+Cb7PlDt7Y1PG8M8VWYhLoIgLb78mbBHcwFbZu0d5zAt1LSX5ilOiWQ==} 40 | peerDependencies: 41 | react: '>=18.0.0' 42 | react-dom: '>=18.0.0' 43 | 44 | client-only@0.0.1: 45 | resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} 46 | 47 | js-tokens@4.0.0: 48 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 49 | 50 | loose-envify@1.4.0: 51 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 52 | hasBin: true 53 | 54 | react-dom@18.3.1: 55 | resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} 56 | peerDependencies: 57 | react: ^18.3.1 58 | 59 | react@18.3.1: 60 | resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} 61 | engines: {node: '>=0.10.0'} 62 | 63 | rescript@11.1.3: 64 | resolution: {integrity: sha512-bI+yxDcwsv7qE34zLuXeO8Qkc2+1ng5ErlSjnUIZdrAWKoGzHXpJ6ZxiiRBUoYnoMsgRwhqvrugIFyNgWasmsw==} 65 | engines: {node: '>=10'} 66 | hasBin: true 67 | 68 | scheduler@0.23.2: 69 | resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} 70 | 71 | swr@2.2.5: 72 | resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} 73 | peerDependencies: 74 | react: ^16.11.0 || ^17.0.0 || ^18.0.0 75 | 76 | use-sync-external-store@1.2.0: 77 | resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} 78 | peerDependencies: 79 | react: ^16.8.0 || ^17.0.0 || ^18.0.0 80 | 81 | snapshots: 82 | 83 | '@rescript/core@1.5.2(rescript@11.1.3)': 84 | dependencies: 85 | rescript: 11.1.3 86 | 87 | '@rescript/react@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': 88 | dependencies: 89 | react: 18.3.1 90 | react-dom: 18.3.1(react@18.3.1) 91 | 92 | client-only@0.0.1: {} 93 | 94 | js-tokens@4.0.0: {} 95 | 96 | loose-envify@1.4.0: 97 | dependencies: 98 | js-tokens: 4.0.0 99 | 100 | react-dom@18.3.1(react@18.3.1): 101 | dependencies: 102 | loose-envify: 1.4.0 103 | react: 18.3.1 104 | scheduler: 0.23.2 105 | 106 | react@18.3.1: 107 | dependencies: 108 | loose-envify: 1.4.0 109 | 110 | rescript@11.1.3: {} 111 | 112 | scheduler@0.23.2: 113 | dependencies: 114 | loose-envify: 1.4.0 115 | 116 | swr@2.2.5(react@18.3.1): 117 | dependencies: 118 | client-only: 0.0.1 119 | react: 18.3.1 120 | use-sync-external-store: 1.2.0(react@18.3.1) 121 | 122 | use-sync-external-store@1.2.0(react@18.3.1): 123 | dependencies: 124 | react: 18.3.1 125 | -------------------------------------------------------------------------------- /prepublish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | NPM_VERSION=$(jq -r ".version" package.json) 4 | BS_VERSION=$(jq -r ".version" rescript.json) 5 | 6 | SWR_VERSION=$(jq ".dependencies.swr" package.json) 7 | 8 | if [ "$NPM_VERSION" != "$BS_VERSION" ]; then 9 | echo "Versions do not match. Exiting..." 10 | exit 1 11 | fi 12 | 13 | read -p "Is SWR version ${SWR_VERSION} corresponding to package \ 14 | version ${NPM_VERSION} correct? (y/n): " -n 1 -r 15 | echo 16 | if [[ $REPLY =~ ^[Nn]$ ]]; then 17 | echo "Aborted publishing process." 18 | exit 1 19 | fi 20 | -------------------------------------------------------------------------------- /rescript.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rescript-swr", 3 | "version": "3.0.0-beta.4", 4 | "sources": [ 5 | { 6 | "dir": "examples", 7 | "subdirs": true, 8 | "type": "dev" 9 | }, 10 | { 11 | "dir": "src", 12 | "subdirs": true 13 | } 14 | ], 15 | "package-specs": { 16 | "module": "esmodule", 17 | "in-source": true 18 | }, 19 | "suffix": ".bs.js", 20 | "jsx": { 21 | "version": 4 22 | }, 23 | "bs-dependencies": [ 24 | "@rescript/core", 25 | "@rescript/react" 26 | ], 27 | "bsc-flags": [ 28 | "-open RescriptCore" 29 | ], 30 | "warnings": { 31 | "error": "+101" 32 | }, 33 | "namespace": false 34 | } 35 | -------------------------------------------------------------------------------- /src/Swr.res: -------------------------------------------------------------------------------- 1 | open SwrCommon 2 | 3 | type cacheState<'data> = { 4 | data?: 'data, 5 | error?: Exn.t, 6 | isLoading?: bool, 7 | isValidating?: bool, 8 | } 9 | 10 | type cache<'data> = { 11 | keys: unit => Iterator.t, 12 | get: string => option>, 13 | set: (string, cacheState<'data>) => unit, 14 | delete: string => unit, 15 | } 16 | 17 | type globalConfig<'key, 'data> = { 18 | provider?: cache<'data>, 19 | ...swrConfiguration<'key, 'data>, 20 | } 21 | 22 | @val @module("swr") 23 | external useSWR: ('arg, fetcher<'arg, 'data>) => swrResponse<'data> = "default" 24 | 25 | @val @module("swr") 26 | external useSWR_config: ( 27 | 'arg, 28 | fetcher<'arg, 'data>, 29 | swrConfiguration<'arg, 'data>, 30 | ) => swrResponse<'data> = "default" 31 | 32 | module SwrConfigProvider = { 33 | @module("swr") @react.component 34 | external make: ( 35 | ~value: globalConfig<'key, 'data> => globalConfig<'key, 'data>, 36 | ~children: React.element, 37 | ) => React.element = "SWRConfig" 38 | 39 | @module("swr") @scope("SWRConfig") 40 | external getDefaultValue: swrConfiguration<'key, 'data> = "defaultValue" 41 | } 42 | 43 | module SwrConfiguration = { 44 | /** `[mutateKey: (config, key)]` broadcasts a revalidation message globally to other SWR hooks */ 45 | @send 46 | external mutateKey: ( 47 | swrConfiguration<'key, 'data>, 48 | @unwrap [#Key('key) | #Filter('key => bool)], 49 | ) => promise<'data> = "mutate" 50 | 51 | /** `[mutateKeyWithData: (config, key, data)]` broadcasts a revalidation message globally to other SWR hooks and replacing with latest data */ 52 | @send 53 | external mutateKeyWithData: ( 54 | swrConfiguration<'key, 'data>, 55 | @unwrap [#Key('key) | #Filter('key => bool)], 56 | option<'data> => option>, 57 | ) => promise<'data> = "mutate" 58 | 59 | /** `[mutateWithOpts: (config, key, data, mutatorOptions)]` is used to update local data programmatically, while revalidating and finally replacing it with the latest data. */ 60 | @send 61 | external mutateWithOpts: ( 62 | swrConfiguration<'key, 'data>, 63 | @unwrap [#Key('key) | #Filter('key => bool)], 64 | option<'data> => option<'data>, 65 | option>, 66 | ) => promise<'data> = "mutate" 67 | 68 | /** `[mutateWithOpts_async: (config, key, data, mutatorOptions)]` is the same as mutateWithOpts, except the data callback returns a promise */ 69 | @send 70 | external mutateWithOpts_async: ( 71 | swrConfiguration<'key, 'data>, 72 | @unwrap [#Key('key) | #Filter('key => bool)], 73 | option<'data> => option>, 74 | option>, 75 | ) => promise<'data> = "mutate" 76 | 77 | @get 78 | external getCache: swrConfiguration<'key, 'data> => cache<'data> = "cache" 79 | 80 | /** `[useSWRConfig]` returns the global configuration, as well as mutate and cache options. */ 81 | @module("swr") 82 | external useSWRConfig: unit => swrConfiguration<'key, 'data> = "useSWRConfig" 83 | } 84 | 85 | /** `[unsafeSetProperty]` is used to unsafely set a configuration property. */ 86 | @set_index 87 | @raises(Exn.t) 88 | external unsafeSetProperty: (swrConfiguration<'key, 'data>, string, 'val) => unit = "" 89 | 90 | /** `[unsafeGetProperty]` is used to unsafely retrieve a configuration property. */ 91 | @get_index 92 | @raises(Exn.t) 93 | external unsafeGetProperty: (swrConfiguration<'key, 'data>, string) => 'val = "" 94 | 95 | @module("swr") 96 | external preload: (string, fetcher<'arg, 'data>) => 'a = "preload" 97 | 98 | @module("swr") @val 99 | external mutate_key: 'key => unit = "mutate" 100 | 101 | @module("swr") @val 102 | external mutate: ('key, 'data, option>) => unit = "mutate" 103 | 104 | @module("swr/immutable") 105 | external useSWRImmutable: ( 106 | 'arg, 107 | fetcher<'arg, 'data>, 108 | swrConfiguration<'arg, 'data>, 109 | ) => swrResponse<'data> = "default" 110 | 111 | module Infinite = SwrInfinite 112 | module Common = SwrCommon 113 | -------------------------------------------------------------------------------- /src/SwrCommon.res: -------------------------------------------------------------------------------- 1 | type fetcher<'arg, 'data> = 'arg => promise<'data> 2 | type fetcher_sync<'arg, 'data> = 'arg => 'data 3 | 4 | type swrHook<'key, 'data, 'config, 'response> = ('key, fetcher<'key, 'data>, 'config) => 'response 5 | 6 | type middleware<'key, 'data, 'config, 'response> = swrHook<'key, 'data, 'config, 'response> => ( 7 | 'key, 8 | fetcher<'key, 'data>, 9 | 'config, 10 | ) => 'response 11 | 12 | type revalidatorOptions = {retryCount?: int, dedupe?: bool} 13 | 14 | type revalidateType = revalidatorOptions => promise> 15 | 16 | type mutatorCallback<'data> = option<'data> => option> 17 | 18 | type mutatorOptions<'data> = { 19 | revalidate?: bool, 20 | populateCache?: (Obj.t, 'data) => 'data, 21 | optimisticData?: 'data => 'data, 22 | rollbackOnError?: Obj.t => bool, 23 | throwOnError?: bool, 24 | } 25 | 26 | type keyedMutator<'data> = ( 27 | mutatorCallback<'data>, 28 | option>, 29 | ) => option> 30 | 31 | type swrResponse<'data> = { 32 | data: option<'data>, 33 | error: option, 34 | mutate: keyedMutator<'data>, 35 | isValidating: bool, 36 | isLoading: bool, 37 | } 38 | 39 | type rec swrConfiguration<'key, 'data> = { 40 | /* Global options */ 41 | dedupingInterval?: int, 42 | errorRetryInterval?: int, 43 | errorRetryCount?: int, 44 | fallbackData?: 'data, 45 | fallback?: Obj.t, 46 | fetcher?: fetcher<'key, 'data>, 47 | focusThrottleInterval?: int, 48 | keepPreviousData?: bool, 49 | loadingTimeout?: int, 50 | refreshInterval?: int, 51 | refreshWhenHidden?: bool, 52 | refreshWhenOffline?: bool, 53 | revalidateOnFocus?: bool, 54 | revalidateOnMount?: bool, 55 | revalidateOnReconnect?: bool, 56 | revalidateIfStale?: bool, 57 | shouldRetryOnError?: bool, 58 | suspense?: bool, 59 | use?: array, swrResponse<'data>>>, 60 | isPaused?: unit => bool, 61 | isOnline?: unit => bool, 62 | isVisible?: unit => bool, 63 | onDiscarded?: string => unit, 64 | onLoadingSlow?: (string, swrConfiguration<'key, 'data>) => unit, 65 | onSuccess?: ('data, string, swrConfiguration<'key, 'data>) => unit, 66 | onError?: (Exn.t, string, swrConfiguration<'key, 'data>) => unit, 67 | onErrorRetry?: ( 68 | Exn.t, 69 | string, 70 | swrConfiguration<'key, 'data>, 71 | revalidateType, 72 | revalidatorOptions, 73 | ) => unit, 74 | compare?: (option<'data>, option<'data>) => bool, 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/SwrEasy.res: -------------------------------------------------------------------------------- 1 | type error = {message: string} 2 | 3 | let makeError: string => error = message => {message: message} 4 | 5 | type deferred<'data> = 6 | Initial | Pending | Refresh(result<'data, error>) | Replete(result<'data, error>) | Invalid 7 | 8 | type mutatorData<'data> = 9 | | Refresh 10 | | Overwrite(option<'data> => option<'data>) 11 | | OverwriteAsync(option<'data> => option>) 12 | | Clear 13 | 14 | type scopedMutatorKey<'key> = Key('key) | Filter('key => bool) 15 | 16 | type scopedMutatorType<'key, 'data> = (scopedMutatorKey<'key>, mutatorData<'data>) 17 | 18 | type keyedMutator<'data> = ( 19 | mutatorData<'data>, 20 | ~revalidate: bool=?, 21 | ~populateCache: (Obj.t, 'data) => 'data=?, 22 | ~optimisticData: 'data => 'data=?, 23 | ~rollbackOnError: Obj.t => bool=?, 24 | ~throwOnError: bool=?, 25 | ) => option> 26 | 27 | type swrResponse<'data> = { 28 | result: deferred<'data>, 29 | mutate: keyedMutator<'data>, 30 | } 31 | 32 | let useSWR = ( 33 | key: 'key, 34 | fetcher: SwrCommon.fetcher<'key, 'data>, 35 | ~config: option>=?, 36 | ): swrResponse<'data> => { 37 | let swr = switch config { 38 | | None => Swr.useSWR(key, fetcher) 39 | | Some(config) => Swr.useSWR_config(key, fetcher, config) 40 | } 41 | 42 | let result = switch swr { 43 | | {data: None, error: None, isValidating: false, isLoading: false} => Initial 44 | | {data: None, error: None, isValidating: _, isLoading: true} => Pending 45 | | {data: Some(data), error: None, isValidating: true, isLoading: _} => Refresh(Ok(data)) 46 | | {data: _, error: Some(err), isValidating: true, isLoading: _} => 47 | switch err->Exn.message { 48 | | None => "Unknown JS Error!" 49 | | Some(err) => err 50 | } 51 | ->makeError 52 | ->Error 53 | ->Refresh 54 | | {data: Some(data), error: None, isValidating: false, isLoading: false} => Replete(Ok(data)) 55 | | {data: None, error: Some(err), isValidating: false, isLoading: false} => 56 | switch err->Exn.message { 57 | | None => "Unknown JS Error!" 58 | | Some(err) => err 59 | } 60 | ->makeError 61 | ->Error 62 | ->Replete 63 | | {data: _, error: _, isValidating: _, isLoading: _} => Invalid 64 | } 65 | 66 | let mutate = ( 67 | mutationType: mutatorData<'data>, 68 | ~revalidate: option=?, 69 | ~populateCache: option<(Obj.t, 'data) => 'data>=?, 70 | ~optimisticData: option<'data => 'data>=?, 71 | ~rollbackOnError: option bool>=?, 72 | ~throwOnError: option=?, 73 | ) => { 74 | // this function exists solely to address a quirk in SWR (and by extension JS) 75 | // where mutations do not work if configuration properties are explicitly set to undefined. 76 | let makeOpts = (obj: {..}) => { 77 | switch revalidate { 78 | | Some(val) => obj->Object.set("revalidate", val) 79 | | _ => () 80 | } 81 | switch populateCache { 82 | | Some(val) => obj->Object.set("populateCache", val) 83 | | _ => () 84 | } 85 | switch optimisticData { 86 | | Some(val) => obj->Object.set("optimisticData", val) 87 | | _ => () 88 | } 89 | switch rollbackOnError { 90 | | Some(val) => obj->Object.set("rollbackOnError", val) 91 | | _ => () 92 | } 93 | switch throwOnError { 94 | | Some(val) => obj->Object.set("throwOnError", val) 95 | | _ => () 96 | } 97 | obj 98 | } 99 | 100 | let opts = makeOpts(Object.make())->Obj.magic // fooling the type system into accepting the opts object 101 | 102 | switch mutationType { 103 | | Refresh => swr.mutate(Obj.magic(), opts) 104 | | Overwrite(cb) => swr.mutate(Obj.magic(cb), opts) 105 | | OverwriteAsync(cb) => swr.mutate(cb, opts) 106 | | Clear => swr.mutate(_ => None, opts) 107 | } 108 | } 109 | 110 | { 111 | result, 112 | mutate, 113 | } 114 | } 115 | 116 | module SwrConfiguration = { 117 | include Swr.SwrConfiguration 118 | 119 | let mutate = ( 120 | config, 121 | key, 122 | data, 123 | ~revalidate=?, 124 | ~populateCache=?, 125 | ~optimisticData=?, 126 | ~rollbackOnError=?, 127 | ~throwOnError=?, 128 | ) => { 129 | open SwrCommon 130 | 131 | let opts = { 132 | ?revalidate, 133 | ?populateCache, 134 | ?optimisticData, 135 | ?rollbackOnError, 136 | ?throwOnError, 137 | } 138 | 139 | config->mutateWithOpts_async( 140 | switch key { 141 | | Key(key) => #Key(key) 142 | | Filter(fn) => #Filter(fn) 143 | }, 144 | switch data { 145 | | Refresh => Obj.magic() 146 | | Overwrite(fn) => 147 | x => { 148 | switch fn(x) { 149 | | None => None 150 | | Some(x) => Obj.magic(x) 151 | } 152 | } 153 | | OverwriteAsync(fn) => fn 154 | | Clear => { 155 | Js.log("clear") 156 | _ => None 157 | } 158 | }, 159 | Some(opts), 160 | ) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/SwrInfinite.res: -------------------------------------------------------------------------------- 1 | open SwrCommon 2 | 3 | type keyLoader<'key, 'any> = (int, option<'any>) => option<'key> 4 | 5 | type swrInfiniteResponse<'data> = { 6 | ...swrResponse<'data>, 7 | size: int, 8 | setSize: (int => int) => promise>>, 9 | } 10 | 11 | type swrInfiniteConfiguration<'key, 'data> = { 12 | ...swrConfiguration<'key, 'data>, 13 | /* infinite hook-specific options */ 14 | initialSize?: int, 15 | revalidateAll?: bool, 16 | persistSize?: bool, 17 | revalidateFirstPage?: bool, 18 | } 19 | 20 | @val @module("swr/infinite") 21 | external useSWRInfinite: ( 22 | keyLoader<'arg, 'any>, 23 | fetcher<'arg, 'data>, 24 | ) => swrInfiniteResponse<'data> = "default" 25 | 26 | @val @module("swr/infinite") 27 | external useSWRInfinite_config: ( 28 | keyLoader<'arg, 'any>, 29 | fetcher<'arg, 'data>, 30 | swrInfiniteConfiguration<'key, 'data>, 31 | ) => swrInfiniteResponse<'data> = "default" 32 | 33 | @ocaml.doc(`[setConfigProperty] is used to unsafely set a configuration property.`) @set_index 34 | external setConfigProperty: (swrInfiniteConfiguration<'key, 'data>, string, 'val) => unit = "" 35 | -------------------------------------------------------------------------------- /src/SwrMutation.res: -------------------------------------------------------------------------------- 1 | type fetcherOpts<'arg> = {arg: 'arg} 2 | type fetcher<'key, 'arg, 'data> = ('key, fetcherOpts<'arg>) => promise<'data> 3 | 4 | type rec swrMutationConfig<'key, 'arg, 'data> = { 5 | fetcher?: fetcher<'key, 'arg, 'data>, 6 | onError?: (Exn.t, 'key, swrMutationConfig<'key, 'arg, 'data>) => unit, 7 | onSuccess?: ('data, 'key, swrMutationConfig<'key, 'arg, 'data>) => unit, 8 | revalidate?: bool, 9 | populateCache?: (Obj.t, 'data) => 'data, 10 | optimisticData?: 'data => 'data, 11 | rollbackOnError?: Obj.t => bool, 12 | throwOnError?: bool, 13 | } 14 | 15 | type swrMutationResponse<'key, 'arg, 'data> = { 16 | data: 'data, 17 | error: Exn.t, 18 | isMutating: bool, 19 | reset: unit => unit, 20 | trigger: ('arg, ~config: swrMutationConfig<'key, 'arg, 'data>=?) => promise<'data>, 21 | } 22 | 23 | @val @module("swr/mutation") 24 | external useSWRMutation: ( 25 | 'key, 26 | fetcher<'key, 'arg, 'data>, 27 | ~config: swrMutationConfig<'key, 'arg, 'data>=?, 28 | ) => swrMutationResponse<'key, 'arg, 'data> = "default" 29 | --------------------------------------------------------------------------------