├── .gitattributes
├── .github
└── FUNDING.yml
├── .gitignore
├── HISTORY.md
├── MIT-LICENSE
├── README.md
├── bsconfig.json
├── examples
├── Index.res
├── LegacyUsage.re
└── index.html
├── package.json
├── src
├── ReactUpdate.res
├── ReactUpdate.resi
├── ReactUpdateLegacy.res
└── ReactUpdateLegacy.resi
└── yarn.lock
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Tell github that .re and .rei files are Reason
2 | *.re linguist-language=Reason
3 | *.rei linguist-language=Reason
4 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: ["bloodyowl"]
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .merlin
3 | .bsb.lock
4 | npm-debug.log
5 | /lib/bs/
6 | /node_modules/
7 | *.bs.js
8 | .cache/*
9 | dist/*
10 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | ## 5.0.2
2 |
3 | Changes:
4 |
5 | - Add compat with React 18 (a1bd63a)
6 |
7 | ## 5.0.1
8 |
9 | Changes:
10 |
11 | - Add compat with ReScript React 0.11.x (6c0d979)
12 |
13 | ## 5.0.0
14 |
15 | Fixes:
16 |
17 | - Fixed effect cleanup (1816f51)
18 |
19 | ## 4.0.0
20 |
21 | Changes:
22 |
23 | - **Breaking change**: the argument order has been changed to match React's APIs (af9447e)
24 |
25 | ## 3.0.2
26 |
27 | Changes:
28 |
29 | - Update React & React DOM peer dependencies (44c6959)
30 |
31 | ## 3.0.1
32 |
33 | Fixes:
34 |
35 | - Fixed `bsconfig` name (thanks @jasim) (2ebd081)
36 |
37 | ## 3.0.0
38 |
39 | Changes:
40 |
41 | - Move to ReScript (62665af)
42 | - Move from reason-react to @rescript/react (62665af)
43 | - Rename project to `rescript-react-update` (62665af)
44 |
45 | ## 2.0.0
46 |
47 | Changes:
48 |
49 | - Make effect management preact-safe (76664b6)
50 |
51 | ## 1.0.0
52 |
53 | Features:
54 |
55 | - Add `ReactUpdateLegacy` for uncancellable effects, easing the migration process if copy pasting from record API reducers (c4ccec5)
56 | - Add `useReducerWithMapState` API to enable lazy state init (19eee9a)
57 |
58 | ## 0.1.1
59 |
60 | Changes:
61 |
62 | - Stop exposing fullState
63 |
64 | ## 0.1.0
65 |
66 | Initial release
67 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2016-2018 Various Authors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rescript-react-update
2 |
3 | > useReducer with updates and side effects!
4 |
5 | ## Installation
6 |
7 | ```console
8 | $ yarn add rescript-react-update
9 | ```
10 |
11 | or
12 |
13 | ```console
14 | $ npm install --save rescript-react-update
15 | ```
16 |
17 | Then add `rescript-react-update` to your `bsconfig.json` `bs-dependencies` field.
18 |
19 | ## ReactUpdate.useReducer
20 |
21 | ```reason
22 | type state = int;
23 |
24 | type action =
25 | | Increment
26 | | Decrement;
27 |
28 | [@react.component]
29 | let make = () => {
30 | let (state, send) =
31 | ReactUpdate.useReducer((state, action) =>
32 | switch (action) {
33 | | Increment => Update(state + 1)
34 | | Decrement => Update(state - 1)
35 | },
36 | 0
37 | );
38 |
39 | {state->React.int}
40 |
41 |
42 |
;
43 | };
44 | ```
45 |
46 | ### Lazy initialisation
47 |
48 | ## ReactUpdate.useReducerWithMapState
49 |
50 | If you'd rather initialize state lazily (if there's some computation you don't want executed at every render for instance), use `useReducerWithMapState` where the first argument is a function taking `unit` and returning the initial state.
51 |
52 | ```reason
53 | type state = int;
54 |
55 | type action =
56 | | Increment
57 | | Decrement;
58 |
59 | [@react.component]
60 | let make = () => {
61 | let (state, send) =
62 | ReactUpdate.useReducerWithMapState(
63 | (state, action) =>
64 | switch (action) {
65 | | Increment => Update(state + 1)
66 | | Decrement => Update(state + 1)
67 | },
68 | () => 0
69 | );
70 |
71 | {state->React.int}
72 |
73 |
74 |
;
75 | };
76 | ```
77 |
78 | ### Cancelling a side effect
79 |
80 | The callback you pass to `SideEffects` & `UpdateWithSideEffect` returns an `option(unit => unit)`, which is the cancellation function.
81 |
82 | ```reason
83 | // doesn't cancel
84 | SideEffects(({send}) => {
85 | Js.log(1);
86 | None
87 | });
88 | // cancels
89 | SideEffects(({send}) => {
90 | let request = Request.make();
91 | request->Future.get(payload => send(Receive(payload)))
92 | Some(() => {
93 | Request.cancel(request)
94 | })
95 | });
96 | ```
97 |
98 | If you want to copy/paste old reducers that don't support cancellation, you can use `ReactUpdateLegacy` instead in place of `ReactUpdate`. Its `SideEffects` and `UpdateWithSideEffects` functions accept functions that return `unit`.
99 |
--------------------------------------------------------------------------------
/bsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rescript-react-update",
3 | "reason": {
4 | "react-jsx": 3
5 | },
6 | "sources": [
7 | {
8 | "dir": "src",
9 | "subdirs": true
10 | },
11 | {
12 | "dir": "examples",
13 | "subdirs": true,
14 | "type": "dev"
15 | }
16 | ],
17 | "package-specs": [
18 | {
19 | "module": "es6",
20 | "in-source": true
21 | }
22 | ],
23 | "warnings": {
24 | "number": "-30"
25 | },
26 | "suffix": ".bs.js",
27 | "bs-dependencies": ["@rescript/react"],
28 | "refmt": 3
29 | }
30 |
--------------------------------------------------------------------------------
/examples/Index.res:
--------------------------------------------------------------------------------
1 | module Counter = {
2 | type state = int
3 | type action = Increment | Decrement
4 | let reducer = (state, action) => {
5 | switch action {
6 | | Increment => ReactUpdate.Update(state + 1)
7 | | Decrement => Update(state - 1)
8 | }
9 | }
10 |
11 | @react.component
12 | let make = () => {
13 | let (state, dispatch) = ReactUpdate.useReducer(reducer, 0)
14 |
15 | {React.int(state)}
16 |
17 |
18 |
19 |
20 |
21 | }
22 | }
23 |
24 | module BasicUsage = {
25 | type action = Tick | Reset
26 | type state = {elapsed: int}
27 |
28 | @react.component
29 | let make = () => {
30 | let (state, send) = ReactUpdate.useReducerWithMapState(
31 | (state, action) =>
32 | switch action {
33 | | Tick =>
34 | UpdateWithSideEffects(
35 | {elapsed: state.elapsed + 1},
36 | ({send}) => {
37 | let timeoutId = Js.Global.setTimeout(() => send(Tick), 1_000)
38 | Some(() => Js.Global.clearTimeout(timeoutId))
39 | },
40 | )
41 | | Reset => Update({elapsed: 0})
42 | },
43 | () => {elapsed: 0},
44 | )
45 | React.useEffect0(() => {
46 | send(Tick)
47 | None
48 | })
49 |
50 | {state.elapsed->Js.String.make->React.string}
51 |
52 |
53 | }
54 | }
55 |
56 | switch ReactDOM.querySelector("#counter") {
57 | | Some(root) => ReactDOM.render(, root)
58 | | None => ()
59 | }
60 |
61 | switch ReactDOM.querySelector("#basic") {
62 | | Some(root) => ReactDOM.render(, root)
63 | | None => ()
64 | }
65 |
--------------------------------------------------------------------------------
/examples/LegacyUsage.re:
--------------------------------------------------------------------------------
1 | type action =
2 | | Tick
3 | | Reset;
4 |
5 | type state = {elapsed: int};
6 |
7 | [@react.component]
8 | let make = () => {
9 | let (state, send) =
10 | ReactUpdateLegacy.useReducer({elapsed: 0}, (action, state) =>
11 | switch (action) {
12 | | Tick =>
13 | UpdateWithSideEffects(
14 | {elapsed: state.elapsed + 1},
15 | ({send}) =>
16 | Js.Global.setTimeout(() => send(Tick), 1_000)->ignore,
17 | )
18 | | Reset => Update({elapsed: 0})
19 | }
20 | );
21 | React.useEffect0(() => {
22 | send(Tick);
23 | None;
24 | });
25 |
26 | {state.elapsed->Js.String.make->React.string}
27 |
28 |
;
29 | };
30 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rescript-react-update",
3 | "version": "5.0.2",
4 | "scripts": {
5 | "build": "bsb -make-world",
6 | "start": "bsb -make-world -w",
7 | "clean": "bsb -clean-world",
8 | "test": "bsb -make-world",
9 | "dev": "bsb -make-world && parcel examples/index.html"
10 | },
11 | "bugs": "https://github.com/bloodyowl/rescript-react-update/issues",
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/bloodyowl/rescript-react-update.git"
15 | },
16 | "keywords": [
17 | "reason-react",
18 | "rescript",
19 | "react"
20 | ],
21 | "author": "bloodyowl ",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "@rescript/react": "^0.10.1 || ^0.11.0",
25 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
26 | "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
27 | },
28 | "devDependencies": {
29 | "@rescript/react": "^0.10.1",
30 | "bs-platform": "^8.3.0",
31 | "parcel-bundler": "^1.12.5",
32 | "react": "^16.8.0",
33 | "react-dom": "^16.8.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/ReactUpdate.res:
--------------------------------------------------------------------------------
1 | open Belt
2 |
3 | type dispatch<'action> = 'action => unit
4 |
5 | type rec update<'action, 'state> =
6 | | NoUpdate
7 | | Update('state)
8 | | UpdateWithSideEffects('state, self<'action, 'state> => option unit>)
9 | | SideEffects(self<'action, 'state> => option unit>)
10 | and self<'action, 'state> = {
11 | send: dispatch<'action>,
12 | dispatch: dispatch<'action>,
13 | state: 'state,
14 | }
15 | and fullState<'action, 'state> = {
16 | state: 'state,
17 | sideEffects: ref => option unit>>>,
18 | }
19 |
20 | type reducer<'state, 'action> = ('state, 'action) => update<'action, 'state>
21 |
22 | let useReducer = (reducer, initialState) => {
23 | let cleanupEffects = React.useRef([])
24 | let ({state, sideEffects}, send) = React.useReducer(({state, sideEffects} as fullState, action) =>
25 | switch reducer(state, action) {
26 | | NoUpdate => fullState
27 | | Update(state) => {...fullState, state: state}
28 | | UpdateWithSideEffects(state, sideEffect) => {
29 | state: state,
30 | sideEffects: ref(Array.concat(sideEffects.contents, [sideEffect])),
31 | }
32 | | SideEffects(sideEffect) => {
33 | ...fullState,
34 | sideEffects: ref(Array.concat(fullState.sideEffects.contents, [sideEffect])),
35 | }
36 | }
37 | , {state: initialState, sideEffects: ref([])})
38 | React.useEffect1(() => {
39 | if Array.length(sideEffects.contents) > 0 {
40 | let sideEffectsToRun = Js.Array.sliceFrom(0, sideEffects.contents)
41 | sideEffects := []
42 | let cancelFuncs = Array.keepMap(sideEffectsToRun, func =>
43 | func({state: state, send: send, dispatch: send})
44 | )
45 | let _ = cleanupEffects.current->Js.Array2.pushMany(cancelFuncs)
46 | }
47 | None
48 | }, [sideEffects])
49 |
50 | React.useEffect0(() => {
51 | Some(() => cleanupEffects.current->Js.Array2.forEach(cb => cb()))
52 | })
53 | (state, send)
54 | }
55 |
56 | let useReducerWithMapState = (reducer, getInitialState) => {
57 | let cleanupEffects = React.useRef([])
58 | let ({state, sideEffects}, send) = React.useReducerWithMapState(
59 | ({state, sideEffects} as fullState, action) =>
60 | switch reducer(state, action) {
61 | | NoUpdate => fullState
62 | | Update(state) => {...fullState, state: state}
63 | | UpdateWithSideEffects(state, sideEffect) => {
64 | state: state,
65 | sideEffects: ref(Array.concat(sideEffects.contents, [sideEffect])),
66 | }
67 | | SideEffects(sideEffect) => {
68 | ...fullState,
69 | sideEffects: ref(Array.concat(fullState.sideEffects.contents, [sideEffect])),
70 | }
71 | },
72 | (),
73 | () => {state: getInitialState(), sideEffects: ref([])},
74 | )
75 | React.useEffect1(() => {
76 | if Array.length(sideEffects.contents) > 0 {
77 | let sideEffectsToRun = Js.Array.sliceFrom(0, sideEffects.contents)
78 | sideEffects := []
79 | let cancelFuncs = Array.keepMap(sideEffectsToRun, func =>
80 | func({state: state, send: send, dispatch: send})
81 | )
82 | let _ = cleanupEffects.current->Js.Array2.pushMany(cancelFuncs)
83 | }
84 | None
85 | }, [sideEffects])
86 |
87 | React.useEffect0(() => {
88 | Some(() => cleanupEffects.current->Js.Array2.forEach(cb => cb()))
89 | })
90 | (state, send)
91 | }
92 |
--------------------------------------------------------------------------------
/src/ReactUpdate.resi:
--------------------------------------------------------------------------------
1 | type dispatch<'action> = 'action => unit
2 |
3 | type rec update<'action, 'state> =
4 | | NoUpdate
5 | | Update('state)
6 | | UpdateWithSideEffects('state, self<'action, 'state> => option unit>)
7 | | SideEffects(self<'action, 'state> => option unit>)
8 | and self<'action, 'state> = {
9 | send: dispatch<'action>,
10 | dispatch: dispatch<'action>,
11 | state: 'state,
12 | }
13 |
14 | type reducer<'state, 'action> = ('state, 'action) => update<'action, 'state>
15 |
16 | let useReducer: (reducer<'state, 'action>, 'state) => ('state, dispatch<'action>)
17 |
18 | let useReducerWithMapState: (reducer<'state, 'action>, () => 'state) => ('state, dispatch<'action>)
19 |
--------------------------------------------------------------------------------
/src/ReactUpdateLegacy.res:
--------------------------------------------------------------------------------
1 | open Belt
2 |
3 | type rec update<'action, 'state> =
4 | | NoUpdate
5 | | Update('state)
6 | | UpdateWithSideEffects('state, self<'action, 'state> => unit)
7 | | SideEffects(self<'action, 'state> => unit)
8 | and self<'action, 'state> = {
9 | send: 'action => unit,
10 | state: 'state,
11 | }
12 | and fullState<'action, 'state> = {
13 | state: 'state,
14 | sideEffects: ref => unit>>,
15 | }
16 |
17 | let useReducer = (initialState, reducer) => {
18 | let ({state, sideEffects}, send) = React.useReducer(({state, sideEffects} as fullState, action) =>
19 | switch reducer(action, state) {
20 | | NoUpdate => fullState
21 | | Update(state) => {...fullState, state: state}
22 | | UpdateWithSideEffects(state, sideEffect) => {
23 | state: state,
24 | sideEffects: ref(Array.concat(sideEffects.contents, [sideEffect])),
25 | }
26 | | SideEffects(sideEffect) => {
27 | ...fullState,
28 | sideEffects: ref(Array.concat(fullState.sideEffects.contents, [sideEffect])),
29 | }
30 | }
31 | , {state: initialState, sideEffects: ref([])})
32 | React.useEffect1(() => {
33 | if Array.length(sideEffects.contents) > 0 {
34 | let sideEffectsToRun = Js.Array.sliceFrom(0, sideEffects.contents)
35 | sideEffects := []
36 | Array.forEach(sideEffectsToRun, func => func({state: state, send: send}))
37 | }
38 | None
39 | }, [sideEffects])
40 | (state, send)
41 | }
42 |
43 | let useReducerWithMapState = (getInitialState, reducer) => {
44 | let ({state, sideEffects}, send) = React.useReducerWithMapState(
45 | ({state, sideEffects} as fullState, action) =>
46 | switch reducer(action, state) {
47 | | NoUpdate => fullState
48 | | Update(state) => {...fullState, state: state}
49 | | UpdateWithSideEffects(state, sideEffect) => {
50 | state: state,
51 | sideEffects: ref(Array.concat(sideEffects.contents, [sideEffect])),
52 | }
53 | | SideEffects(sideEffect) => {
54 | ...fullState,
55 | sideEffects: ref(Array.concat(fullState.sideEffects.contents, [sideEffect])),
56 | }
57 | },
58 | (),
59 | () => {state: getInitialState(), sideEffects: ref([])},
60 | )
61 | React.useEffect1(() => {
62 | if Array.length(sideEffects.contents) > 0 {
63 | let sideEffectsToRun = Js.Array.sliceFrom(0, sideEffects.contents)
64 | sideEffects := []
65 | Array.forEach(sideEffectsToRun, func => func({state: state, send: send}))
66 | }
67 | None
68 | }, [sideEffects])
69 | (state, send)
70 | }
71 |
--------------------------------------------------------------------------------
/src/ReactUpdateLegacy.resi:
--------------------------------------------------------------------------------
1 | type rec update<'action, 'state> =
2 | | NoUpdate
3 | | Update('state)
4 | | UpdateWithSideEffects('state, self<'action, 'state> => unit)
5 | | SideEffects(self<'action, 'state> => unit)
6 | and self<'action, 'state> = {
7 | send: 'action => unit,
8 | state: 'state,
9 | }
10 |
11 | let useReducer: ('state, ('action, 'state) => update<'action, 'state>) => ('state, 'action => unit)
12 |
13 | let useReducerWithMapState: (
14 | unit => 'state,
15 | ('action, 'state) => update<'action, 'state>,
16 | ) => ('state, 'action => unit)
17 |
--------------------------------------------------------------------------------