├── .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 | --------------------------------------------------------------------------------