}> = "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