├── .editorconfig ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── bower.json ├── docs ├── advanced.md ├── beginner.md └── getting-started.md ├── package-lock.json ├── package.json ├── packages.dhall ├── spago.dhall ├── spago.test.dhall ├── src └── React │ └── Basic │ ├── Hooks.js │ ├── Hooks.purs │ └── Hooks │ ├── Aff.purs │ ├── ErrorBoundary.js │ ├── ErrorBoundary.purs │ ├── Internal.purs │ ├── ResetToken.purs │ ├── Suspense.js │ ├── Suspense.purs │ └── Suspense │ └── Store.purs └── test ├── Main.purs └── Spec ├── MemoSpec.purs ├── React18HooksSpec.purs ├── UseReducerSpec.purs └── UseStateSpec.purs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = false 9 | 10 | [Makefile] 11 | indent_style = tab -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build & Test 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js 19 | uses: actions/setup-node@v2 20 | with: 21 | cache: "npm" 22 | - run: npm ci 23 | - run: npm run deps --if-present 24 | - run: npm run build --if-present 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.spago/ 4 | /.pulp-cache/ 5 | /output/ 6 | /output-pulp 7 | /generated-docs/ 8 | /.psc-package/ 9 | /.psc* 10 | /.purs* 11 | /.psa* 12 | /.vscode/ 13 | /.log/ 14 | /.history 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Madeline Trotter 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-basic-hooks [![Build Status](https://github.com/spicydonuts/purescript-react-basic-hooks/actions/workflows/node.js.yml/badge.svg)](https://github.com/spicydonuts/purescript-react-basic-hooks/actions/workflows/node.js.yml) 2 | 3 | `react-basic-hooks` is a React hook API for [react-basic](https://github.com/lumihq/purescript-react-basic). 4 | 5 | _Note:_ This API relies on React `>=16.8.0`. For more info on hooks, see [React's documentation](https://reactjs.org/docs/hooks-intro.html). 6 | 7 | I recommend using PureScript's "qualified do" syntax while using this library (it's used in the examples, the `React.do` bits). 8 | It became available in the `0.12.2` compiler release. 9 | 10 | This library provides the `React.Basic.Hooks` module, which can completely replace the `React.Basic` module. 11 | It borrows a few types from the current `React.Basic` module like `ReactComponent` and `JSX` to make it easy to use both versions in the same project. 12 | If we prefer this API over the existing react-basic API, we may eventually replace `React.Basic` with this implementation. 13 | 14 | ## Example 15 | 16 | ```purs 17 | mkCounter :: Component Int 18 | mkCounter = do 19 | component "Counter" \initialValue -> React.do 20 | counter /\ setCounter <- useState initialValue 21 | 22 | pure 23 | $ R.button 24 | { onClick: handler_ do 25 | setCounter (_ + 1) 26 | , children: 27 | [ R.text $ "Increment: " <> show counter ] 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-react-basic-hooks", 3 | "license": [ 4 | "Apache-2.0" 5 | ], 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/megamaddu/purescript-react-basic-hooks" 9 | }, 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "output" 15 | ], 16 | "dependencies": { 17 | "purescript-aff": "^v7.0.0", 18 | "purescript-aff-promise": "^v4.0.0", 19 | "purescript-bifunctors": "^v6.0.0", 20 | "purescript-console": "^v6.0.0", 21 | "purescript-control": "^v6.0.0", 22 | "purescript-datetime": "^v6.0.0", 23 | "purescript-effect": "^v4.0.0", 24 | "purescript-either": "^v6.1.0", 25 | "purescript-exceptions": "^v6.0.0", 26 | "purescript-foldable-traversable": "^v6.0.0", 27 | "purescript-functions": "^v6.0.0", 28 | "purescript-indexed-monad": "^v2.1.0", 29 | "purescript-integers": "^v6.0.0", 30 | "purescript-maybe": "^v6.0.0", 31 | "purescript-newtype": "^v5.0.0", 32 | "purescript-now": "^v6.0.0", 33 | "purescript-nullable": "^v6.0.0", 34 | "purescript-ordered-collections": "^v3.0.0", 35 | "purescript-prelude": "^v6.0.0", 36 | "purescript-react-basic": "^v17.0.0", 37 | "purescript-refs": "^v6.0.0", 38 | "purescript-tuples": "^v7.0.0", 39 | "purescript-type-equality": "^v4.0.1", 40 | "purescript-unsafe-coerce": "^v6.0.0", 41 | "purescript-unsafe-reference": "^v5.0.0", 42 | "purescript-web-html": "^v4.0.0" 43 | }, 44 | "resolutions": { 45 | "purescript-control": "^v6.0.0", 46 | "purescript-newtype": "^v5.0.0", 47 | "purescript-prelude": "^v6.0.0", 48 | "purescript-unsafe-coerce": "^v6.0.0", 49 | "purescript-safe-coerce": "^2.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced Topics 2 | 3 | Coming soon! -------------------------------------------------------------------------------- /docs/beginner.md: -------------------------------------------------------------------------------- 1 | # Beginner Topics 2 | 3 | Coming soon! -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | I assume you're here because you want to write a React app using PureScript! Good, good, because that's what this guide is all about. 4 | 5 | Before we begin you'll need to install [Node.js](https://nodejs.org/). If you aren't sure what editor to use, try [VSCode](https://code.visualstudio.com/). It's got great PureScript language support via the [PureScript IDE](https://marketplace.visualstudio.com/items?itemName=nwolverson.ide-purescript) extension. 6 | 7 | Done? Ok, we're going to set up this project from scratch so you have a basic idea of how all the parts work together. Don't worry, it won't take long! 8 | 9 | ## Creating a PureScript web project 10 | 11 | Let's create a directory for the project. 12 | 13 | ```sh 14 | mkdir purs-react-app && cd purs-react-app 15 | ``` 16 | 17 | Initialize `npm` (comes with Node.js). The command below creates a `package.json` file (and a `package-lock.json` file, but that's for `npm` to manage) to track our JavaScript dependencies (i.e. React) and dev tooling (PureScript, etc). We'll also put some useful scripts in here later. 18 | 19 | ```sh 20 | npm init -y 21 | ``` 22 | 23 | Install PureScript and Spago (project management tool for PureScript). 24 | 25 | ```sh 26 | npm i -D purescript spago 27 | ``` 28 | 29 | Next, use Spago to set up the PureScript files we need to get started. Spago is installed locally via `npm`, so we need to prefix this command with `npx` to run it. 30 | 31 | ```sh 32 | npx spago init 33 | ``` 34 | 35 | You should now have a few new directories and files. `packages.dhall` defines the PureScript package source the project will use. The default will be fine. `spago.dhall` is where our PureScript dependencies are defined. Let's add `react-basic-hooks` to that list using Spago. We'll grab `react-basic-dom` too, since we'll need it soon. 36 | 37 | ```sh 38 | npx spago install react-basic-hooks react-basic-dom 39 | ``` 40 | 41 | We also need to install React and ReactDOM, since `react-basic-hooks` and `react-basic-dom` rely on them, respectively. 42 | 43 | ```sh 44 | npm i -S react react-dom 45 | ``` 46 | 47 | Ok, we're almost done.. We're now able to write React code using PureScript, but the only thing we can do with it is compile it to JavaScript and stare at it! Which is actually a good time and you'll learn a lot about PureScript in doing so, but let's save that for a future guide. 48 | 49 | What we really want next is to start a server with a basic HTML skeleton so we can actually run our React app! 50 | 51 | We can set this up pretty quick with existing bundlers like Parcel. Let's install it. 52 | 53 | ```sh 54 | npm i -D parcel 55 | ``` 56 | 57 | Parcel's a bit picky about project setup. We're building an app, not a JavaScript library. Remove the following line from the `package.json` file to appease it.. :pray: 58 | 59 | ```json 60 | "main": "index.js", 61 | ``` 62 | 63 | Now we need an entry-point HTML file to point Parcel at. Create the file `src/index.html` with the following content. 64 | 65 | ```html 66 | 67 | 68 | 69 | 70 | 71 | 72 | PureScript React App 73 | 74 | 75 | 76 |
77 | 78 | 79 | ``` 80 | 81 | Now that HTML file is pointing to a mysterious `index.js` file! Create `src/index.js` with this content. 82 | 83 | ```js 84 | import { main } from "../output/Main"; 85 | 86 | main(); 87 | ``` 88 | 89 | More mysteries! What's this "Main" file with a "main" function inside!? 90 | 91 | It's our PureScript! Let's take a look at the `src/Main.purs` file Spago created for us earlier. It probably looks like this. 92 | 93 | ```purs 94 | module Main where 95 | 96 | import Prelude 97 | 98 | import Effect (Effect) 99 | import Effect.Console (log) 100 | 101 | main :: Effect Unit 102 | main = do 103 | log "🍝" 104 | ``` 105 | 106 | Let's change that `log` call at the bottom so it logs something with deep, personal significance. We'll be that much more proud of our work once we see it in action. 107 | 108 | ```purs 109 | main :: Effect Unit 110 | main = do 111 | log "guuu" 112 | ``` 113 | 114 | ## Building and running the app 115 | 116 | Building is a two-step process. The PureScript compiler (via Spago) handles the PureScript, emitting it as JavaScript in the `output/` folder. Parcel handles everything from that point on, serving (in dev mode), bundling, and minifying the JavaScript side. 117 | 118 | The first thing you should always do is build your PureScript once, like so. 119 | 120 | ```sh 121 | npx spago build 122 | ``` 123 | 124 | Then you can run Parcel in "serve" mode to see it all running in your browser (`ctrl + c` to quit). 125 | 126 | ```sh 127 | npx parcel serve src/index.html 128 | ``` 129 | 130 | Parcel will give you a URL for your dev server. Open it and take a look at the console. If everything went as planned, you should feel an inspiring sense of accomplishment! Good job! What? You wanted more than an empty white web page? We're getting there, soon, soon! 131 | 132 | If you change your HTML or JavaScript files (including by rebuilding the PureScript code), you'll see those changes live-reload in your browser. Nice. 133 | 134 | Let's create a production build. 135 | 136 | ```sh 137 | npx parcel build --dist-dir dist src/index.html 138 | ``` 139 | 140 | This command directs the bundled output to the `dist/` folder. Add this folder to the `.gitignore` Spago made for us earlier! 141 | 142 | ```sh 143 | echo "dist/" >> .gitignore 144 | ``` 145 | 146 | Ignore Parcel's cache dir as well. 147 | 148 | ```sh 149 | echo ".parcel-cache/" >> .gitignore 150 | ``` 151 | 152 | You can deploy that `dist/` directory to a server, or upload it to S3 or GitHub Pages. The possibilities are endless! 153 | 154 | For example, you can use `npx` to install and run the `serve` tool, a quick and easy way to spin up a server in a given directory (`ctrl + c` to quit). 155 | 156 | ```sh 157 | npx serve dist 158 | ``` 159 | 160 | Parcel can do quite a bit for you. I recommend [learning more about it](https://parceljs.org/docs/) (or whatever bundler or dev server you choose) when you have a chance. 161 | 162 | ## Making "future you's" life easier 163 | 164 | Remembering all these commands is a pain. Let's make some shortcuts. 165 | 166 | You can add scripts to your `package.json` and invoke them with `npm`. These scripts don't need the `npx` prefix. 167 | 168 | ```json 169 | { 170 | "name": "purs-react-app", 171 | "version": "1.0.0", 172 | "description": "", 173 | "scripts": { 174 | "test": "spago test", 175 | "start": "spago build && parcel serve src/index.html", 176 | "build": "spago build && parcel build --dist-dir dist src/index.html" 177 | }, 178 | "keywords": [], 179 | "author": "", 180 | "license": "ISC", 181 | "devDependencies": { 182 | "parcel": "^2.0.1", 183 | "purescript": "^0.14.5", 184 | "spago": "^0.20.3" 185 | }, 186 | "dependencies": { 187 | "react": "^17.0.2", 188 | "react-dom": "^17.0.2" 189 | } 190 | } 191 | ``` 192 | 193 | The `start` and `test` scripts are special, you can run them like this: `npm start` or `npm test` 194 | 195 | Any other commands you add need the "run" prefix: `npm run build` 196 | 197 | We haven't talked about `spago test` yet. You can learn more about general PureScript testing elsewhere. Testing PureScript React components will be a separate guide at some point. 198 | 199 | ## Rendering your first component 200 | 201 | Since this is your first component, I'll write it for you. Don't worry, there's an entire guide on how to write your own components. I'll send you there next! 202 | 203 | Copy this content into `src/App/Pages/Home.purs`. You can read it if you like, but don't sweat the details. 204 | 205 | ```purs 206 | module App.Pages.Home where 207 | 208 | import Prelude 209 | 210 | import React.Basic.DOM as DOM 211 | import React.Basic.DOM.Events (capture_) 212 | import React.Basic.Hooks (Component, component, useState, (/\)) 213 | import React.Basic.Hooks as React 214 | 215 | type HomeProps = Unit 216 | 217 | mkHome :: Component HomeProps 218 | mkHome = do 219 | component "Home" \_props -> React.do 220 | 221 | counter /\ setCounter <- useState 0 222 | 223 | pure $ DOM.div 224 | { children: 225 | [ DOM.h1_ [ DOM.text "Home" ] 226 | , DOM.p_ [ DOM.text "Try clicking the button!" ] 227 | , DOM.button 228 | { onClick: capture_ do 229 | setCounter (_ + 1) 230 | , children: 231 | [ DOM.text "Clicks: " 232 | , DOM.text (show counter) 233 | ] 234 | } 235 | ] 236 | } 237 | ``` 238 | 239 | If you're already familiar with writing React components in JavaScript and you squint this will probably look familiar! 240 | 241 | Now we need to replace the content of `src/Main.purs` so it renders our new component. 242 | 243 | ```purs 244 | module Main where 245 | 246 | import Prelude 247 | 248 | import App.Pages.Home (mkHome) 249 | import Data.Maybe (Maybe(..)) 250 | import Effect (Effect) 251 | import Effect.Exception (throw) 252 | import React.Basic.DOM (render) 253 | import Web.DOM.NonElementParentNode (getElementById) 254 | import Web.HTML (window) 255 | import Web.HTML.HTMLDocument (toNonElementParentNode) 256 | import Web.HTML.Window (document) 257 | 258 | main :: Effect Unit 259 | main = do 260 | root <- getElementById "root" =<< (map toNonElementParentNode $ document =<< window) 261 | case root of 262 | Nothing -> 263 | throw "Root element not found." 264 | Just r -> do 265 | home <- mkHome 266 | render (home unit) r 267 | ``` 268 | 269 | Once you're done, run `npm start`. Spago's going to complain that we're now using additional modules we didn't explicitly depend on. Go ahead and run the command it suggests. It should look like this (don't forget to prefix with `npx`). 270 | 271 | ```sh 272 | npx spago install exceptions maybe web-dom web-html 273 | ``` 274 | 275 | Now run `npm start` again and open the link. 276 | 277 | If you see the click counting button you've succeeded! You're ready to head to the [beginner guide](beginner.md) to learn how to build your own components! Good luck! 278 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-react-basic-hooks", 3 | "version": "1.0.0", 4 | "description": "`react-basic-hooks` is a React hook API for [react-basic](https://github.com/lumihq/purescript-react-basic)!", 5 | "main": "index.js", 6 | "module": "true", 7 | "dependencies": { 8 | "react": "^18.1.0", 9 | "react-dom": "^18.1.0" 10 | }, 11 | "devDependencies": { 12 | "@testing-library/react": "^13.2.0", 13 | "@testing-library/user-event": "^14.2.0", 14 | "bower": "^1.8.14", 15 | "global-jsdom": "^8.4.0", 16 | "jsdom": "^19.0.0", 17 | "jsdom-global": "^3.0.2", 18 | "npm-run-all": "^4.1.5", 19 | "pulp": "^16.0.1", 20 | "purescript": "^0.15.2", 21 | "spago": "^0.20.9" 22 | }, 23 | "scripts": { 24 | "deps": "run-s deps:*", 25 | "deps:spago": "spago install", 26 | "deps:pulp": "bower install", 27 | "build": "run-s build:*", 28 | "build:spago": "spago build", 29 | "build:pulp": "pulp build -o output-pulp", 30 | "test": "spago -x spago.test.dhall test", 31 | "clean": "rm -rf .spago bower_components output output-pulp node_modules .pulp-cache .psci_modules" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/spicydonuts/purescript-react-basic-hooks.git" 36 | }, 37 | "keywords": [], 38 | "author": "Madeline Trotter", 39 | "license": "Apache-2.0", 40 | "bugs": { 41 | "url": "https://github.com/spicydonuts/purescript-react-basic-hooks/issues" 42 | }, 43 | "homepage": "https://github.com/spicydonuts/purescript-react-basic-hooks#readme" 44 | } 45 | -------------------------------------------------------------------------------- /packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220531/packages.dhall 3 | sha256:278d3608439187e51136251ebf12fabda62d41ceb4bec9769312a08b56f853e3 4 | 5 | in upstream 6 | with react-testing-library = 7 | { dependencies = 8 | [ "aff" 9 | , "aff-promise" 10 | , "control" 11 | , "effect" 12 | , "exceptions" 13 | , "foldable-traversable" 14 | , "foreign" 15 | , "functions" 16 | , "identity" 17 | , "maybe" 18 | , "prelude" 19 | , "react-basic" 20 | , "spec" 21 | , "strings" 22 | , "transformers" 23 | , "unsafe-coerce" 24 | , "web-dom" 25 | , "web-events" 26 | , "web-html" 27 | ] 28 | , repo = 29 | "https://github.com/i-am-the-slime/purescript-react-testing-library" 30 | , version = "v4.0.1" 31 | } 32 | -------------------------------------------------------------------------------- /spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "react-basic-hooks" 6 | , dependencies = 7 | [ "aff" 8 | , "aff-promise" 9 | , "bifunctors" 10 | , "console" 11 | , "control" 12 | , "datetime" 13 | , "effect" 14 | , "either" 15 | , "exceptions" 16 | , "foldable-traversable" 17 | , "functions" 18 | , "indexed-monad" 19 | , "integers" 20 | , "maybe" 21 | , "newtype" 22 | , "now" 23 | , "nullable" 24 | , "ordered-collections" 25 | , "prelude" 26 | , "react-basic" 27 | , "refs" 28 | , "tuples" 29 | , "type-equality" 30 | , "unsafe-coerce" 31 | , "unsafe-reference" 32 | , "web-html" 33 | ] 34 | , packages = ./packages.dhall 35 | , sources = [ "src/**/*.purs" ] 36 | , license = "Apache-2.0" 37 | , repository = "https://github.com/megamaddu/purescript-react-basic-hooks" 38 | } 39 | -------------------------------------------------------------------------------- /spago.test.dhall: -------------------------------------------------------------------------------- 1 | let conf = ./spago.dhall 2 | 3 | in conf // { 4 | sources = conf.sources # [ "test/**/*.purs" ], 5 | dependencies = conf.dependencies # 6 | [ "react-testing-library" 7 | , "react-basic-dom" 8 | , "spec" 9 | , "spec-discovery" 10 | , "foreign-object" 11 | , "web-dom" 12 | , "arrays" 13 | , "strings" 14 | , "debug" 15 | , "tailrec" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/React/Basic/Hooks.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const useEqCache = (eq, a) => { 4 | const memoRef = React.useRef(a); 5 | if (memoRef.current !== a && !eq(memoRef.current, a)) { 6 | memoRef.current = a; 7 | } 8 | return memoRef.current; 9 | }; 10 | 11 | export function reactChildrenToArray(children) { 12 | return React.Children.toArray(children); 13 | } 14 | 15 | export const memo_ = React.memo; 16 | export const memoEq_ = React.memo; 17 | 18 | export function useState_(tuple, initialState) { 19 | const [state, setState] = React.useState( 20 | typeof initialState === "function" ? () => initialState : initialState 21 | ); 22 | if (!setState.hasOwnProperty("$$reactBasicHooks$$cachedSetState")) { 23 | setState.$$reactBasicHooks$$cachedSetState = (update) => () => 24 | setState(update); 25 | } 26 | return tuple(state, setState.$$reactBasicHooks$$cachedSetState); 27 | } 28 | 29 | export function useEffect_(eq, deps, effect) { 30 | const memoizedKey = useEqCache(eq, deps); 31 | React.useEffect(effect, [memoizedKey]); 32 | } 33 | 34 | export function useEffectAlways_(effect) { 35 | return React.useEffect(effect); 36 | } 37 | 38 | export function useLayoutEffect_(eq, deps, effect) { 39 | const memoizedKey = useEqCache(eq, deps); 40 | React.useLayoutEffect(effect, [memoizedKey]); 41 | } 42 | 43 | export function useLayoutEffectAlways_(effect) { 44 | return React.useLayoutEffect(effect); 45 | } 46 | 47 | export function useInsertionEffect_(eq, deps, effect) { 48 | const memoizedKey = useEqCache(eq, deps); 49 | React.useInsertionEffect(effect, [memoizedKey]); 50 | } 51 | 52 | export function useInsertionEffectAlways_(effect) { 53 | React.useInsertionEffect(effect); 54 | } 55 | 56 | export function useReducer_(tuple, reducer, initialState) { 57 | const [state, dispatch] = React.useReducer(reducer, initialState); 58 | if (!dispatch.hasOwnProperty("$$reactBasicHooks$$cachedDispatch")) { 59 | dispatch.$$reactBasicHooks$$cachedDispatch = (action) => () => 60 | dispatch(action); 61 | } 62 | return tuple(state, dispatch.$$reactBasicHooks$$cachedDispatch); 63 | } 64 | 65 | export const useRef_ = React.useRef; 66 | 67 | export function readRef_(ref) { 68 | return ref.current; 69 | } 70 | 71 | export function writeRef_(ref, a) { 72 | ref.current = a; 73 | } 74 | 75 | export const useContext_ = React.useContext; 76 | export { useEqCache as useEqCache_ }; 77 | 78 | export function useMemo_(eq, deps, computeA) { 79 | const memoizedKey = useEqCache(eq, deps); 80 | return React.useMemo(computeA, [memoizedKey]); 81 | } 82 | 83 | export const useDebugValue_ = React.useDebugValue; 84 | 85 | export const useId_ = React.useId 86 | 87 | export function useTransition_(tuple) { 88 | const [isPending, startTransitionImpl] = React.useTransition() 89 | const startTransition = (update) => () => startTransitionImpl(update) 90 | return tuple(isPending, startTransition); 91 | } 92 | 93 | export const useDeferredValue_ = React.useDeferredValue 94 | 95 | export const useSyncExternalStore2_ = React.useSyncExternalStore 96 | export const useSyncExternalStore3_ = React.useSyncExternalStore 97 | 98 | export function unsafeSetDisplayName(displayName, component) { 99 | component.displayName = displayName; 100 | component.toString = () => displayName; 101 | return component; 102 | } 103 | 104 | export function displayName(component) { 105 | return typeof component === "string" 106 | ? component 107 | : component.displayName || "[unknown]"; 108 | } 109 | -------------------------------------------------------------------------------- /src/React/Basic/Hooks.purs: -------------------------------------------------------------------------------- 1 | module React.Basic.Hooks 2 | ( Component 3 | , component 4 | , reactComponent 5 | , reactComponentWithChildren 6 | , reactComponentFromHook 7 | , ReactChildren 8 | , reactChildrenToArray 9 | , reactChildrenFromArray 10 | , memo 11 | , memo' 12 | , useState 13 | , useState' 14 | , UseState 15 | , useEffect 16 | , useEffectOnce 17 | , useEffectAlways 18 | , UseEffect 19 | , useLayoutEffect 20 | , useLayoutEffectOnce 21 | , useLayoutEffectAlways 22 | , UseLayoutEffect 23 | , useInsertionEffect 24 | , useInsertionEffectOnce 25 | , useInsertionEffectAlways 26 | , UseInsertionEffect 27 | , Reducer 28 | , mkReducer 29 | , runReducer 30 | , useReducer 31 | , UseReducer 32 | , readRef 33 | , readRefMaybe 34 | , writeRef 35 | , useRef 36 | , UseRef 37 | , useContext 38 | , UseContext 39 | , useEqCache 40 | , UseEqCache 41 | , useMemo 42 | , UseMemo 43 | , useDebugValue 44 | , UseDebugValue 45 | , useId 46 | , UseId 47 | , useTransition 48 | , UseTransition 49 | , useDeferredValue 50 | , UseDeferredValue 51 | , useSyncExternalStore 52 | , useSyncExternalStore' 53 | , UseSyncExternalStore 54 | , UnsafeReference(..) 55 | , displayName 56 | , module React.Basic.Hooks.Internal 57 | , module React.Basic 58 | , module Data.Tuple.Nested 59 | ) where 60 | 61 | import Prelude hiding (bind, discard) 62 | 63 | import Data.Bifunctor (rmap) 64 | import Data.Function.Uncurried (Fn2, mkFn2, runFn2) 65 | import Data.Maybe (Maybe) 66 | import Data.Newtype (class Newtype) 67 | import Data.Nullable (Nullable, toMaybe) 68 | import Data.Tuple (Tuple(..)) 69 | import Data.Tuple.Nested (type (/\), (/\)) 70 | import Effect (Effect) 71 | import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, runEffectFn1, runEffectFn2, runEffectFn3) 72 | import Prelude (bind) as Prelude 73 | import Prim.Row (class Lacks) 74 | import React.Basic (JSX, ReactComponent, ReactContext, Ref, consumer, contextConsumer, contextProvider, createContext, element, elementKeyed, empty, keyed, fragment, provider) 75 | import React.Basic.Hooks.Internal (Hook, HookApply, Pure, Render, bind, discard, coerceHook, unsafeHook, unsafeRenderEffect, type (&)) 76 | import Unsafe.Coerce (unsafeCoerce) 77 | import Unsafe.Reference (unsafeRefEq) 78 | 79 | --| A simple type alias to clean up component definitions. 80 | type Component props 81 | = Effect (props -> JSX) 82 | 83 | --| Create a component function given a display name and render function. 84 | --| Creating components is effectful because React uses the function 85 | --| instance as the component's "identity" or "type". Components should 86 | --| be created during a bootstrap phase and not within component 87 | --| lifecycles or render functions. 88 | component :: 89 | forall hooks props. 90 | String -> 91 | (props -> Render Unit hooks JSX) -> 92 | Component props 93 | component name renderFn = Prelude.do 94 | c <- reactComponent name (renderFn <<< _.nested) 95 | pure (element c <<< { nested: _ }) 96 | 97 | --| Create a React component given a display name and render function. 98 | --| Creating components is effectful because React uses the function 99 | --| instance as the component's "identity" or "type". Components should 100 | --| be created during a bootstrap phase and not within component 101 | --| lifecycles or render functions. See `componentWithChildren` if 102 | --| you need to use the `children` prop. 103 | reactComponent :: 104 | forall hooks props. 105 | Lacks "children" props => 106 | Lacks "key" props => 107 | Lacks "ref" props => 108 | String -> 109 | ({ | props } -> Render Unit hooks JSX) -> 110 | Effect (ReactComponent { | props }) 111 | reactComponent = unsafeReactComponent 112 | 113 | --| Create a React component given a display name and render function. 114 | --| This is the same as `component` but allows the use of the `children` 115 | --| prop. 116 | reactComponentWithChildren :: 117 | forall hooks props children. 118 | Lacks "key" props => 119 | Lacks "ref" props => 120 | String -> 121 | ({ children :: ReactChildren children | props } -> Render Unit hooks JSX) -> 122 | Effect (ReactComponent { children :: ReactChildren children | props }) 123 | reactComponentWithChildren = unsafeReactComponent 124 | 125 | --| Convert a hook to a render-prop component. The value returned from the 126 | --| hook will be passed to the `render` prop, a function from that value 127 | --| to `JSX`. 128 | --| 129 | --| This function is useful for consuming a hook within a non-hook component. 130 | reactComponentFromHook :: 131 | forall hooks props r. 132 | Lacks "children" props => 133 | Lacks "key" props => 134 | Lacks "ref" props => 135 | String -> 136 | ({ render :: r -> JSX | props } -> Hook hooks r) -> 137 | Effect (ReactComponent { render :: r -> JSX | props }) 138 | reactComponentFromHook name propsToHook = do 139 | reactComponent name \props -> map props.render $ propsToHook props 140 | 141 | unsafeReactComponent :: 142 | forall hooks props. 143 | Lacks "key" props => 144 | Lacks "ref" props => 145 | String -> 146 | ({ | props } -> Render Unit hooks JSX) -> 147 | Effect (ReactComponent { | props }) 148 | unsafeReactComponent name renderFn = 149 | let 150 | c = 151 | unsafeReactFunctionComponent 152 | ( mkEffectFn1 153 | ( \props -> 154 | unsafeDiscardRenderEffects (renderFn props) 155 | ) 156 | ) 157 | in 158 | runEffectFn2 unsafeSetDisplayName name c 159 | 160 | unsafeDiscardRenderEffects :: forall x y a. Render x y a -> Effect a 161 | unsafeDiscardRenderEffects = unsafeCoerce 162 | 163 | unsafeReactFunctionComponent :: forall props. EffectFn1 props JSX -> ReactComponent props 164 | unsafeReactFunctionComponent = unsafeCoerce 165 | 166 | data ReactChildren :: forall k. k -> Type 167 | data ReactChildren a 168 | 169 | foreign import reactChildrenToArray :: forall a. ReactChildren a -> Array a 170 | 171 | reactChildrenFromArray :: forall a. Array a -> ReactChildren a 172 | reactChildrenFromArray = unsafeCoerce 173 | 174 | --| Prevents a component from re-rendering if its new props are referentially 175 | --| equal to its old props (not value-based equality -- this is due to the 176 | --| underlying React implementation). 177 | --| Prefer `memo'` for more PureScript-friendldy behavior. 178 | memo :: 179 | forall props. 180 | Effect (ReactComponent props) -> 181 | Effect (ReactComponent props) 182 | memo = flip Prelude.bind (runEffectFn1 memo_) 183 | 184 | --| Similar to `memo` but takes a function to compare previous and new props. 185 | --| For example: 186 | --| 187 | --| ```purs 188 | --| mkMyComponent :: Effect (ReactComponent { id :: Int }) 189 | --| mkMyComponent = 190 | --| memo' eq do 191 | --| reactComponent "MyComponent" \{ id } -> React.do 192 | --| ... 193 | --| ``` 194 | memo' :: 195 | forall props. 196 | (props -> props -> Boolean) -> 197 | Effect (ReactComponent props) -> 198 | Effect (ReactComponent props) 199 | memo' arePropsEqual comp = Prelude.do 200 | c <- comp 201 | runEffectFn2 memoEq_ c (mkFn2 arePropsEqual) 202 | 203 | useState :: 204 | forall state. 205 | state -> 206 | Hook (UseState state) (state /\ ((state -> state) -> Effect Unit)) 207 | useState initialState = 208 | unsafeHook do 209 | runEffectFn2 useState_ (mkFn2 Tuple) initialState 210 | 211 | useState' :: 212 | forall state. 213 | state -> 214 | Hook (UseState state) (state /\ (state -> Effect Unit)) 215 | useState' initialState = useState initialState <#> rmap (_ <<< const) 216 | 217 | foreign import data UseState :: Type -> Type -> Type 218 | 219 | --| Runs the given effect when the component is mounted and any time the given 220 | --| dependencies change. The effect should return its cleanup function. For 221 | --| example, if the effect registers a global event listener, it should return 222 | --| an Effect which removes the listener. 223 | --| 224 | --| ```purs 225 | --| useEffect deps do 226 | --| timeoutId <- setTimeout 1000 (logShow deps) 227 | --| pure (clearTimeout timeoutId) 228 | --| ``` 229 | --| 230 | --| If no cleanup is needed, use `pure (pure unit)` or `pure mempty` to return 231 | --| a no-op Effect 232 | --| 233 | --| ```purs 234 | --| useEffect deps do 235 | --| logShow deps 236 | --| pure mempty 237 | --| ``` 238 | useEffect :: 239 | forall deps. 240 | Eq deps => 241 | deps -> 242 | Effect (Effect Unit) -> 243 | Hook (UseEffect deps) Unit 244 | useEffect deps effect = 245 | unsafeHook do 246 | runEffectFn3 useEffect_ (mkFn2 eq) deps effect 247 | 248 | --| Like `useEffect`, but the effect is only performed a single time per component 249 | --| instance. Prefer `useEffect` with a proper dependency list whenever possible! 250 | useEffectOnce :: Effect (Effect Unit) -> Hook (UseEffect Unit) Unit 251 | useEffectOnce effect = unsafeHook (runEffectFn3 useEffect_ (mkFn2 \_ _ -> true) unit effect) 252 | 253 | --| Like `useEffect`, but the effect is performed on every render. Prefer `useEffect` 254 | --| with a proper dependency list whenever possible! 255 | useEffectAlways :: Effect (Effect Unit) -> Hook (UseEffect Unit) Unit 256 | useEffectAlways effect = unsafeHook (runEffectFn1 useEffectAlways_ effect) 257 | 258 | foreign import data UseEffect :: Type -> Type -> Type 259 | 260 | --| Like `useEffect`, but the effect is performed synchronously after the browser has 261 | --| calculated layout. Useful for reading properties from the DOM that are not available 262 | --| before layout, such as element sizes and positions. Prefer `useEffect` whenever 263 | --| possible to avoid blocking browser painting. 264 | useLayoutEffect :: 265 | forall deps. 266 | Eq deps => 267 | deps -> 268 | Effect (Effect Unit) -> 269 | Hook (UseLayoutEffect deps) Unit 270 | useLayoutEffect deps effect = unsafeHook (runEffectFn3 useLayoutEffect_ (mkFn2 eq) deps effect) 271 | 272 | --| Like `useLayoutEffect`, but the effect is only performed a single time per component 273 | --| instance. Prefer `useLayoutEffect` with a proper dependency list whenever possible! 274 | useLayoutEffectOnce :: Effect (Effect Unit) -> Hook (UseLayoutEffect Unit) Unit 275 | useLayoutEffectOnce effect = unsafeHook (runEffectFn3 useLayoutEffect_ (mkFn2 \_ _ -> true) unit effect) 276 | 277 | --| Like `useLayoutEffect`, but the effect is performed on every render. Prefer `useLayoutEffect` 278 | --| with a proper dependency list whenever possible! 279 | useLayoutEffectAlways :: Effect (Effect Unit) -> Hook (UseLayoutEffect Unit) Unit 280 | useLayoutEffectAlways effect = unsafeHook (runEffectFn1 useLayoutEffectAlways_ effect) 281 | 282 | foreign import data UseLayoutEffect :: Type -> Type -> Type 283 | 284 | useInsertionEffect :: 285 | forall deps. 286 | Eq deps => 287 | deps -> 288 | Effect (Effect Unit) -> 289 | Hook (UseInsertionEffect deps) Unit 290 | useInsertionEffect deps effect = unsafeHook (runEffectFn3 useInsertionEffect_ (mkFn2 eq) deps effect) 291 | 292 | --| Like `useInsertionEffect`, but the effect is only performed a single time per component 293 | --| instance. Prefer `useInsertionEffect` with a proper dependency list whenever possible! 294 | useInsertionEffectOnce :: Effect (Effect Unit) -> Hook (UseInsertionEffect Unit) Unit 295 | useInsertionEffectOnce effect = unsafeHook (runEffectFn3 useInsertionEffect_ (mkFn2 \_ _ -> true) unit effect) 296 | 297 | --| Like `useInsertionEffect`, but the effect is performed on every render. Prefer `useInsertionEffect` 298 | --| with a proper dependency list whenever possible! 299 | useInsertionEffectAlways :: Effect (Effect Unit) -> Hook (UseInsertionEffect Unit) Unit 300 | useInsertionEffectAlways effect = unsafeHook (runEffectFn1 useInsertionEffectAlways_ effect) 301 | 302 | foreign import data UseInsertionEffect :: Type -> Type -> Type 303 | 304 | newtype Reducer state action 305 | = Reducer (Fn2 state action state) 306 | 307 | --| Creating reducer functions for React is effectful because 308 | --| React uses the function instance's reference to optimize 309 | --| rendering behavior. 310 | mkReducer :: forall state action. (state -> action -> state) -> Effect (Reducer state action) 311 | mkReducer = pure <<< Reducer <<< mkFn2 312 | 313 | --| Run a wrapped `Reducer` function as a normal function (like `runFn2`). 314 | --| Useful for testing, simulating actions, or building more complicated 315 | --| hooks on top of `useReducer` 316 | runReducer :: forall state action. Reducer state action -> state -> action -> state 317 | runReducer (Reducer reducer) = runFn2 reducer 318 | 319 | --| Use `mkReducer` to construct a reducer function. 320 | useReducer :: 321 | forall state action. 322 | state -> 323 | Reducer state action -> 324 | Hook (UseReducer state action) (state /\ (action -> Effect Unit)) 325 | useReducer initialState (Reducer reducer) = 326 | unsafeHook do 327 | runEffectFn3 useReducer_ 328 | (mkFn2 Tuple) 329 | reducer 330 | initialState 331 | 332 | foreign import data UseReducer :: Type -> Type -> Type -> Type 333 | 334 | useRef :: forall a. a -> Hook (UseRef a) (Ref a) 335 | useRef initialValue = 336 | unsafeHook do 337 | runEffectFn1 useRef_ initialValue 338 | 339 | readRef :: forall a. Ref a -> Effect a 340 | readRef = runEffectFn1 readRef_ 341 | 342 | readRefMaybe :: forall a. Ref (Nullable a) -> Effect (Maybe a) 343 | readRefMaybe a = map toMaybe (readRef a) 344 | 345 | writeRef :: forall a. Ref a -> a -> Effect Unit 346 | writeRef = runEffectFn2 writeRef_ 347 | 348 | foreign import data UseRef :: Type -> Type -> Type 349 | 350 | useContext :: forall a. ReactContext a -> Hook (UseContext a) a 351 | useContext context = unsafeHook (runEffectFn1 useContext_ context) 352 | 353 | foreign import data UseContext :: Type -> Type -> Type 354 | 355 | --| Cache an instance of a value, replacing it when `eq` returns `false`. 356 | --| 357 | --| This is a low-level performance optimization tool. It can be useful 358 | --| for optimizing a component's props for use with `memo`, where 359 | --| JavaScript instance equality matters. 360 | useEqCache :: 361 | forall a. 362 | Eq a => 363 | a -> 364 | Hook (UseEqCache a) a 365 | useEqCache a = 366 | unsafeHook do 367 | runEffectFn2 useEqCache_ (mkFn2 eq) a 368 | 369 | foreign import data UseEqCache :: Type -> Type -> Type 370 | 371 | --| Lazily compute a value. The result is cached until the `deps` change. 372 | useMemo :: 373 | forall deps a. 374 | Eq deps => 375 | deps -> 376 | (Unit -> a) -> 377 | Hook (UseMemo deps a) a 378 | useMemo deps computeA = 379 | unsafeHook do 380 | runEffectFn3 useMemo_ (mkFn2 eq) deps computeA 381 | 382 | foreign import data UseMemo :: Type -> Type -> Type -> Type 383 | 384 | --| Use this hook to display a label for custom hooks in React DevTools 385 | useDebugValue :: forall a. a -> (a -> String) -> Hook (UseDebugValue a) Unit 386 | useDebugValue debugValue display = unsafeHook (runEffectFn2 useDebugValue_ debugValue display) 387 | 388 | foreign import data UseDebugValue :: Type -> Type -> Type 389 | 390 | foreign import data UseId :: Type -> Type 391 | useId :: Hook UseId String 392 | useId = unsafeHook useId_ 393 | 394 | foreign import data UseTransition :: Type -> Type 395 | useTransition :: 396 | Hook UseTransition (Boolean /\ ((Effect Unit) -> Effect Unit)) 397 | useTransition = unsafeHook $ runEffectFn1 useTransition_ (mkFn2 Tuple) 398 | 399 | foreign import data UseDeferredValue :: Type -> Type -> Type 400 | useDeferredValue :: forall a. a -> Hook (UseDeferredValue a) a 401 | useDeferredValue a = unsafeHook $ runEffectFn1 useDeferredValue_ a 402 | 403 | foreign import data UseSyncExternalStore :: Type -> Type -> Type 404 | useSyncExternalStore :: forall a. 405 | ((Effect Unit) -> Effect (Effect Unit)) 406 | -> (Effect a) 407 | -> (Effect a) 408 | -> Hook (UseSyncExternalStore a) a 409 | useSyncExternalStore subscribe getSnapshot getServerSnapshot = 410 | unsafeHook $ 411 | runEffectFn3 useSyncExternalStore3_ 412 | (mkEffectFn1 subscribe) 413 | getSnapshot 414 | getServerSnapshot 415 | useSyncExternalStore' :: forall a. 416 | ((Effect Unit) -> Effect (Effect Unit)) 417 | -> (Effect a) 418 | -> Hook (UseSyncExternalStore a) a 419 | useSyncExternalStore' subscribe getSnapshot = 420 | unsafeHook $ 421 | runEffectFn2 useSyncExternalStore2_ (mkEffectFn1 subscribe) getSnapshot 422 | 423 | newtype UnsafeReference a 424 | = UnsafeReference a 425 | 426 | derive instance newtypeUnsafeReference :: Newtype (UnsafeReference a) _ 427 | 428 | instance eqUnsafeReference :: Eq (UnsafeReference a) where 429 | eq = unsafeRefEq 430 | 431 | --| Retrieve the Display Name from a `ReactComponent`. Useful for debugging and improving 432 | --| error messages in logs. 433 | --| 434 | --| __*See also:* `component`__ 435 | foreign import displayName :: 436 | forall props. 437 | ReactComponent props -> 438 | String 439 | 440 | --| 441 | --| Internal utility or FFI functions 442 | --| 443 | foreign import memo_ :: 444 | forall props. 445 | EffectFn1 446 | (ReactComponent props) 447 | (ReactComponent props) 448 | 449 | foreign import memoEq_ :: 450 | forall props. 451 | EffectFn2 452 | (ReactComponent props) 453 | (Fn2 props props Boolean) 454 | (ReactComponent props) 455 | 456 | foreign import unsafeSetDisplayName :: 457 | forall props. 458 | EffectFn2 String (ReactComponent props) (ReactComponent props) 459 | 460 | foreign import useState_ :: 461 | forall state. 462 | EffectFn2 463 | (forall a b. Fn2 a b (a /\ b)) 464 | state 465 | (state /\ ((state -> state) -> Effect Unit)) 466 | 467 | foreign import useEffect_ :: 468 | forall deps. 469 | EffectFn3 470 | (Fn2 deps deps Boolean) 471 | deps 472 | (Effect (Effect Unit)) 473 | Unit 474 | 475 | foreign import useEffectAlways_ :: 476 | EffectFn1 477 | (Effect (Effect Unit)) 478 | Unit 479 | 480 | foreign import useLayoutEffect_ :: 481 | forall deps. 482 | EffectFn3 483 | (Fn2 deps deps Boolean) 484 | deps 485 | (Effect (Effect Unit)) 486 | Unit 487 | 488 | foreign import useLayoutEffectAlways_ :: 489 | EffectFn1 490 | (Effect (Effect Unit)) 491 | Unit 492 | 493 | foreign import useInsertionEffect_ :: 494 | forall deps. 495 | EffectFn3 496 | (Fn2 deps deps Boolean) 497 | deps 498 | (Effect (Effect Unit)) 499 | Unit 500 | 501 | foreign import useInsertionEffectAlways_ :: 502 | EffectFn1 503 | (Effect (Effect Unit)) 504 | Unit 505 | 506 | foreign import useReducer_ :: 507 | forall state action. 508 | EffectFn3 509 | (forall a b. Fn2 a b (a /\ b)) 510 | (Fn2 state action state) 511 | state 512 | (state /\ (action -> Effect Unit)) 513 | 514 | foreign import readRef_ :: 515 | forall a. 516 | EffectFn1 517 | (Ref a) 518 | a 519 | 520 | foreign import writeRef_ :: 521 | forall a. 522 | EffectFn2 523 | (Ref a) 524 | a 525 | Unit 526 | 527 | foreign import useRef_ :: 528 | forall a. 529 | EffectFn1 530 | a 531 | (Ref a) 532 | 533 | foreign import useContext_ :: 534 | forall a. 535 | EffectFn1 536 | (ReactContext a) 537 | a 538 | 539 | foreign import useEqCache_ :: 540 | forall a. 541 | EffectFn2 542 | (Fn2 a a Boolean) 543 | a 544 | a 545 | 546 | foreign import useMemo_ :: 547 | forall deps a. 548 | EffectFn3 549 | (Fn2 deps deps Boolean) 550 | deps 551 | (Unit -> a) 552 | a 553 | 554 | foreign import useDebugValue_ :: 555 | forall a. 556 | EffectFn2 557 | a 558 | (a -> String) 559 | Unit 560 | 561 | foreign import useId_ :: Effect String 562 | 563 | foreign import useTransition_ 564 | :: forall a b. EffectFn1 (Fn2 a b (a /\ b)) 565 | (Boolean /\ ((Effect Unit) -> Effect Unit)) 566 | 567 | foreign import useDeferredValue_ :: forall a. EffectFn1 a a 568 | 569 | foreign import useSyncExternalStore2_ :: forall a. EffectFn2 570 | (EffectFn1 (Effect Unit) (Effect Unit)) -- subscribe 571 | (Effect a) -- getSnapshot 572 | a 573 | 574 | foreign import useSyncExternalStore3_ :: forall a. EffectFn3 575 | (EffectFn1 (Effect Unit) (Effect Unit)) -- subscribe 576 | (Effect a) -- getSnapshot 577 | (Effect a) -- getServerSnapshot 578 | a -------------------------------------------------------------------------------- /src/React/Basic/Hooks/Aff.purs: -------------------------------------------------------------------------------- 1 | module React.Basic.Hooks.Aff 2 | ( useAff 3 | , useSteppingAff 4 | , UseAff(..) 5 | , useAffReducer 6 | , AffReducer 7 | , mkAffReducer 8 | , runAffReducer 9 | , noEffects 10 | , UseAffReducer(..) 11 | ) where 12 | 13 | import Prelude 14 | 15 | import Data.Either (Either(..)) 16 | import Data.Foldable (for_) 17 | import Data.Function.Uncurried (Fn2, mkFn2, runFn2) 18 | import Data.Maybe (Maybe(..)) 19 | import Data.Newtype (class Newtype) 20 | import Effect (Effect) 21 | import Effect.Aff (Aff, Error, error, killFiber, launchAff, launchAff_, throwError, try) 22 | import Effect.Class (liftEffect) 23 | import Effect.Unsafe (unsafePerformEffect) 24 | import React.Basic.Hooks (type (&), type (/\), Hook, Reducer, UnsafeReference(..), UseEffect, UseMemo, UseReducer, UseState, coerceHook, mkReducer, unsafeRenderEffect, useEffect, useMemo, useReducer, useState, (/\)) 25 | import React.Basic.Hooks as React 26 | 27 | --| `useAff` is used for asynchronous effects or `Aff`. The asynchronous effect 28 | --| is re-run whenever the deps change. If another `Aff` runs when the deps 29 | --| change before the previous async resolves, it will cancel the previous 30 | --| in-flight effect. 31 | --| 32 | --| *Note: This hook requires parent components to handle error states! Don't 33 | --| forget to implement a React error boundary or avoid `Aff` errors entirely 34 | --| by incorporating them into your result type!* 35 | useAff :: 36 | forall deps a. 37 | Eq deps => 38 | deps -> 39 | Aff a -> 40 | Hook (UseAff deps a) (Maybe a) 41 | useAff = useAff' (const Nothing) 42 | 43 | --| A variant of `useAff` where the asynchronous effect's result is preserved up 44 | --| until the next run of the asynchronous effect _completes_. 45 | --| 46 | --| Contrast this with `useAff`, where the asynchronous effect's result is 47 | --| preserved only up until the next run of the asynchronous effect _starts_. 48 | useSteppingAff :: 49 | forall deps a. 50 | Eq deps => 51 | deps -> 52 | Aff a -> 53 | Hook (UseAff deps a) (Maybe a) 54 | useSteppingAff = useAff' identity 55 | 56 | useAff' :: 57 | forall deps a. 58 | Eq deps => 59 | (Maybe (Either Error a) -> Maybe (Either Error a)) -> 60 | deps -> 61 | Aff a -> 62 | Hook (UseAff deps a) (Maybe a) 63 | useAff' initUpdater deps aff = 64 | coerceHook React.do 65 | result /\ setResult <- useState Nothing 66 | useEffect deps do 67 | setResult initUpdater 68 | fiber <- 69 | launchAff do 70 | r <- try aff 71 | liftEffect do 72 | setResult \_ -> Just r 73 | pure do 74 | launchAff_ do 75 | killFiber (error "Stale request cancelled") fiber 76 | unsafeRenderEffect case result of 77 | Just (Left err) -> throwError err 78 | Just (Right a) -> pure (Just a) 79 | Nothing -> pure Nothing 80 | 81 | newtype UseAff deps a hooks 82 | = UseAff 83 | ( hooks 84 | & UseState (Maybe (Either Error a)) 85 | & UseEffect deps 86 | ) 87 | 88 | derive instance ntUseAff :: Newtype (UseAff deps a hooks) _ 89 | 90 | --| Provide an initial state and a reducer function. This is a more powerful 91 | --| version of `useReducer`, where a state change can additionally queue 92 | --| asynchronous operations. The results of those operations must be mapped 93 | --| into the reducer's `action` type. This is essentially the Elm architecture. 94 | --| 95 | --| Generally, I recommend `useAff` paired with tools like `useResetToken` over 96 | --| `useAffReducer` as there are many ways `useAffReducer` can result in race 97 | --| conditions. `useAff` with proper dependency management will handle previous 98 | --| request cancellation and ensure your `Aff` result is always in sync with 99 | --| the provided `deps`, for example. To accomplish the same thing with 100 | --| `useAffReducer` would require tracking `Fiber`s manually in your state 101 | --| somehow.. :c 102 | --| 103 | --| That said, `useAffReducer` can still be helpful when converting from the 104 | --| current `React.Basic` (non-hooks) API or for those used to Elm. 105 | --| 106 | --| *Note: Aff failures are thrown. If you need to capture an error state, be 107 | --| sure to capture it in your action type!* 108 | useAffReducer :: 109 | forall state action. 110 | state -> 111 | AffReducer state action -> 112 | Hook (UseAffReducer state action) (state /\ (action -> Effect Unit)) 113 | useAffReducer initialState affReducer = 114 | coerceHook React.do 115 | reducer' <- 116 | useMemo (UnsafeReference affReducer) \_ -> 117 | unsafePerformEffect do 118 | mkReducer (\{ state } -> runAffReducer affReducer state) 119 | { state, effects } /\ dispatch <- 120 | useReducer { state: initialState, effects: [] } reducer' 121 | useEffect (UnsafeReference effects) do 122 | for_ effects \aff -> 123 | launchAff_ do 124 | actions <- aff 125 | liftEffect do for_ actions dispatch 126 | mempty 127 | pure (state /\ dispatch) 128 | 129 | newtype UseAffReducer state action hooks 130 | = UseAffReducer 131 | ( hooks 132 | & UseMemo (UnsafeReference (AffReducer state action)) 133 | ( Reducer 134 | { effects :: Array (Aff (Array action)) 135 | , state :: state 136 | } 137 | action 138 | ) 139 | & UseReducer { state :: state, effects :: Array (Aff (Array action)) } action 140 | & UseEffect (UnsafeReference (Array (Aff (Array action)))) 141 | ) 142 | 143 | derive instance ntUseAffReducer :: Newtype (UseAffReducer state action hooks) _ 144 | 145 | newtype AffReducer state action 146 | = AffReducer 147 | ( Fn2 148 | state 149 | action 150 | { state :: state, effects :: Array (Aff (Array action)) } 151 | ) 152 | 153 | mkAffReducer :: 154 | forall state action. 155 | (state -> action -> { state :: state, effects :: Array (Aff (Array action)) }) -> 156 | Effect (AffReducer state action) 157 | mkAffReducer = pure <<< AffReducer <<< mkFn2 158 | 159 | --| Run a wrapped `Reducer` function as a normal function (like `runFn2`). 160 | --| Useful for testing, simulating actions, or building more complicated 161 | --| hooks on top of `useReducer` 162 | runAffReducer :: 163 | forall state action. 164 | AffReducer state action -> 165 | state -> 166 | action -> 167 | { state :: state, effects :: Array (Aff (Array action)) } 168 | runAffReducer (AffReducer reducer) = runFn2 reducer 169 | 170 | noEffects :: 171 | forall state action. 172 | state -> 173 | { state :: state 174 | , effects :: Array (Aff (Array action)) 175 | } 176 | noEffects state = { state, effects: [] } 177 | -------------------------------------------------------------------------------- /src/React/Basic/Hooks/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function errorBoundary_(name) { 4 | return () => { 5 | class ErrorBoundary extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { error: null }; 9 | } 10 | render() { 11 | return this.props.render({ 12 | error: this.state.error, 13 | dismissError: () => this.setState({ error: null }) 14 | }); 15 | } 16 | } 17 | ErrorBoundary.displayName = name; 18 | ErrorBoundary.getDerivedStateFromError = (error) => ({ error }); 19 | return ErrorBoundary; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/React/Basic/Hooks/ErrorBoundary.purs: -------------------------------------------------------------------------------- 1 | module React.Basic.Hooks.ErrorBoundary 2 | ( mkErrorBoundary 3 | ) where 4 | 5 | import Prelude 6 | import Data.Maybe (Maybe) 7 | import Data.Nullable (Nullable, toMaybe) 8 | import Effect (Effect) 9 | import Effect.Aff (Error) 10 | import React.Basic.Hooks (JSX, ReactComponent, element) 11 | 12 | --| Create a React error boundary with the given name. The resulting 13 | --| component takes a render callback which exposes the error if one 14 | --| exists and an effect for dismissing the error. 15 | mkErrorBoundary :: 16 | String -> 17 | Effect 18 | ( ({ error :: Maybe Error, dismissError :: Effect Unit } -> JSX) -> 19 | JSX 20 | ) 21 | mkErrorBoundary name = do 22 | c <- errorBoundary_ name 23 | pure $ element c <<< mapProps 24 | where 25 | mapProps render = 26 | { render: 27 | \{ error, dismissError } -> 28 | render { error: toMaybe error, dismissError } 29 | } 30 | 31 | foreign import errorBoundary_ :: 32 | String -> 33 | Effect 34 | ( ReactComponent 35 | { render :: 36 | { error :: Nullable Error 37 | , dismissError :: Effect Unit 38 | } -> 39 | JSX 40 | } 41 | ) 42 | -------------------------------------------------------------------------------- /src/React/Basic/Hooks/Internal.purs: -------------------------------------------------------------------------------- 1 | module React.Basic.Hooks.Internal 2 | ( Render 3 | , coerceHook 4 | , unsafeHook 5 | , unsafeRenderEffect 6 | , Pure 7 | , Hook 8 | , bind 9 | , discard 10 | , HookApply 11 | , type (&) 12 | ) where 13 | 14 | import Prelude hiding (bind) 15 | 16 | import Control.Applicative.Indexed (class IxApplicative) 17 | import Control.Apply.Indexed (class IxApply) 18 | import Control.Bind.Indexed (class IxBind, ibind) 19 | import Control.Monad.Indexed (class IxMonad) 20 | import Data.Functor.Indexed (class IxFunctor) 21 | import Data.Newtype (class Newtype) 22 | import Effect (Effect) 23 | import Prelude (bind) as Prelude 24 | import Type.Equality (class TypeEquals) 25 | 26 | --| Render represents the effects allowed within a React component's 27 | --| body, i.e. during "render". This includes hooks and ends with 28 | --| returning JSX (see `pure`), but does not allow arbitrary side 29 | --| effects. 30 | --| 31 | --| The `x` and `y` type arguments represent the stack of effects that this 32 | --| `Render` implements, with `x` being the stack at the start of this 33 | --| `Render`, and `y` the stack at the end. 34 | --| 35 | --| See 36 | --| [purescript-indexed-monad](https://pursuit.purescript.org/packages/purescript-indexed-monad) 37 | --| to understand how the order of the stack is enforced at the type level. 38 | newtype Render :: Type -> Type -> Type -> Type 39 | newtype Render x y a 40 | = Render (Effect a) 41 | 42 | --| Rename/alias a chain of hooks. Useful for exposing a single 43 | --| "clean" type when creating a hook to improve error messages 44 | --| and hide implementation details, particularly for libraries 45 | --| hiding internal info. 46 | --| 47 | --| For example, the following alias is technically correct but 48 | --| when inspecting types or error messages the alias is expanded 49 | --| to the full original type and `UseAff` is never seen: 50 | --| 51 | --| ```purs 52 | --| type UseAff deps a hooks 53 | --| = UseEffect deps (UseState (Result a) hooks) 54 | --| 55 | --| useAff :: ... -> Hook (UseAff deps a) (Result a) 56 | --| useAff deps aff = React.do 57 | --| ... 58 | --| ``` 59 | --| 60 | --| `coerceHook` allows the same code to safely export a newtype 61 | --| instead, hiding the internal implementation: 62 | --| 63 | --| ```purs 64 | --| newtype UseAff deps a hooks 65 | --| = UseAff (UseEffect deps (UseState (Result a) hooks)) 66 | --| 67 | --| derive instance ntUseAff :: Newtype (UseAff deps a hooks) _ 68 | --| 69 | --| useAff :: ... -> Hook (UseAff deps a) (Result a) 70 | --| useAff deps aff = coerceHook React.do 71 | --| ... 72 | --| ``` 73 | --| 74 | --| 75 | --| 76 | coerceHook :: 77 | forall hooks oldHook newHook a. 78 | Newtype newHook oldHook => 79 | Render hooks oldHook a -> 80 | Render hooks newHook a 81 | coerceHook (Render a) = Render a 82 | 83 | --| Promote an arbitrary Effect to a Hook. 84 | --| 85 | --| This is unsafe because it allows arbitrary 86 | --| effects to be performed during a render, which 87 | --| may cause them to be run many times by React. 88 | --| This function is primarily for constructing 89 | --| new hooks using the FFI. If you just want to 90 | --| alias a safe hook's effects, prefer `coerceHook`. 91 | --| 92 | --| It's also unsafe because the author of the hook 93 | --| type (the `newHook` type variable used here) _MUST_ 94 | --| contain all relevant types. For example, `UseState` 95 | --| has a phantom type to track the type of the value contained. 96 | --| `useEffect` tracks the type used as the deps. `useAff` tracks 97 | --| both the deps and the resulting response's type. Forgetting 98 | --| to do this allows the consumer to reorder hook effects. If 99 | --| `useState` didn't track the return type the following 100 | --| extremely unsafe code would be allowed: 101 | --| 102 | --| ```purs 103 | --| React.do 104 | --| if xyz then 105 | --| _ <- useState 0 106 | --| useState Nothing 107 | --| else 108 | --| s <- useState Nothing 109 | --| _ <- useState 0 110 | --| pure s 111 | --| ... 112 | --| ``` 113 | --| 114 | --| The same applies to `deps` in these examples as they use 115 | --| `Eq` and a reorder would allow React to pass incorrect 116 | --| types into the `eq` function! 117 | unsafeHook :: 118 | forall newHook a. 119 | Effect a -> Hook newHook a 120 | unsafeHook = Render 121 | 122 | --| Promote an arbitrary Effect to a Pure render effect. 123 | --| 124 | --| This is unsafe because it allows arbitrary 125 | --| effects to be performed during a render, which 126 | --| may cause them to be run many times by React. 127 | --| You should almost always prefer `useEffect`! 128 | unsafeRenderEffect :: forall a. Effect a -> Pure a 129 | unsafeRenderEffect = Render 130 | 131 | --| Type alias used to lift otherwise pure functionality into the Render type. 132 | --| Not commonly used. 133 | type Pure a 134 | = forall hooks. Render hooks hooks a 135 | 136 | --| Type alias for Render representing a hook. 137 | --| 138 | --| The `newHook` argument is a type constructor which takes a set of existing 139 | --| effects and generates a type with a new set of effects (produced by this 140 | --| hook) stacked on top. 141 | type Hook (newHook :: Type -> Type) a 142 | = forall hooks. Render hooks (newHook hooks) a 143 | 144 | instance ixFunctorRender :: IxFunctor Render where 145 | imap f (Render a) = Render (map f a) 146 | 147 | instance ixApplyRender :: IxApply Render where 148 | iapply (Render f) (Render a) = Render (apply f a) 149 | 150 | instance ixApplicativeRender :: IxApplicative Render where 151 | ipure a = Render (pure a) 152 | 153 | instance ixBindRender :: IxBind Render where 154 | ibind (Render m) f = Render (Prelude.bind m \a -> case f a of Render b -> b) 155 | 156 | instance ixMonadRender :: IxMonad Render 157 | 158 | --| Exported for use with qualified-do syntax 159 | bind :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b 160 | bind = ibind 161 | 162 | --| Exported for use with qualified-do syntax 163 | discard :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b 164 | discard = ibind 165 | 166 | instance functorRender :: Functor (Render x y) where 167 | map f (Render a) = Render (map f a) 168 | 169 | instance applyRender :: TypeEquals x y => Apply (Render x y) where 170 | apply (Render f) (Render a) = Render (apply f a) 171 | 172 | instance applicativeRender :: TypeEquals x y => Applicative (Render x y) where 173 | pure a = Render (pure a) 174 | 175 | instance bindRender :: TypeEquals x y => Bind (Render x y) where 176 | bind (Render m) f = Render (Prelude.bind m \a -> case f a of Render b -> b) 177 | 178 | instance monadRender :: TypeEquals x y => Monad (Render x y) 179 | 180 | instance semigroupRender :: (TypeEquals x y, Semigroup a) => Semigroup (Render x y a) where 181 | append (Render a) (Render b) = Render (append a b) 182 | 183 | instance monoidRender :: (TypeEquals x y, Monoid a) => Monoid (Render x y a) where 184 | mempty = Render mempty 185 | 186 | type HookApply hooks (newHook :: Type -> Type) 187 | = newHook hooks 188 | 189 | --| Applies a new hook to a hook chain, with the innermost hook as the left argument. 190 | --| This allows hook chains to be written in reverse order, aligning them with the 191 | --| order they appear when actually used in do-notation. 192 | --| ```purescript 193 | --| type UseCustomHook hooks = UseEffect String (UseState Int hooks) 194 | --| type UseCustomHook' hooks = hooks & UseState Int & UseEffect String 195 | --| ``` 196 | infixl 0 type HookApply as & -------------------------------------------------------------------------------- /src/React/Basic/Hooks/ResetToken.purs: -------------------------------------------------------------------------------- 1 | module React.Basic.Hooks.ResetToken 2 | ( UseResetToken(..) 3 | , ResetToken 4 | , useResetToken 5 | ) where 6 | 7 | import Prelude 8 | 9 | import Data.Newtype (class Newtype) 10 | import Effect (Effect) 11 | import React.Basic.Hooks (type (/\), Hook, UseState, coerceHook, useState, (/\)) 12 | import React.Basic.Hooks as React 13 | 14 | --| Useful for resetting effects or component state. A `ResetToken` can be 15 | --| used alongside other hook dependencies to force a reevaluation of 16 | --| whatever depends on those dependencies. 17 | --| 18 | --| For example, consider an effect or API call which depends on the state 19 | --| of a search bar. You may want a button in the UI to refresh stale data. 20 | --| In this case you would include a `ResetToken` in your search effect/aff's 21 | --| dependencies and call `useResetToken`'s reset effect in the button's 22 | --| `onClick` handler. 23 | useResetToken :: Hook UseResetToken (ResetToken /\ (Effect Unit)) 24 | useResetToken = 25 | coerceHook React.do 26 | resetToken /\ setResetToken <- useState 0 27 | let reset = setResetToken (_ + 1) 28 | pure (ResetToken resetToken /\ reset) 29 | 30 | newtype UseResetToken hooks 31 | = UseResetToken (UseState Int hooks) 32 | 33 | derive instance ntUseResetToken :: Newtype (UseResetToken hooks) _ 34 | 35 | newtype ResetToken = ResetToken Int 36 | 37 | derive newtype instance eqResetToken :: Eq ResetToken 38 | 39 | instance showResetToken :: Show ResetToken where 40 | show (ResetToken token) = "(ResetToken " <> show token <> ")" 41 | -------------------------------------------------------------------------------- /src/React/Basic/Hooks/Suspense.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const suspense_ = React.Suspense; 4 | -------------------------------------------------------------------------------- /src/React/Basic/Hooks/Suspense.purs: -------------------------------------------------------------------------------- 1 | module React.Basic.Hooks.Suspense 2 | ( suspend 3 | , Suspended(..) 4 | , SuspenseResult(..) 5 | , suspense 6 | ) where 7 | 8 | import Prelude 9 | import Control.Promise (Promise) 10 | import Control.Promise as Promise 11 | import Effect (Effect) 12 | import Effect.Aff (Error, Fiber, joinFiber, throwError) 13 | import React.Basic.Hooks (JSX, Pure, ReactComponent, element, unsafeRenderEffect) 14 | import Unsafe.Coerce (unsafeCoerce) 15 | 16 | --| Suspend rendering until a result exists. 17 | --| 18 | --| *Note: Error and loading states are thrown to React! Don't forget 19 | --| to implement a React error boundary and ensure `suspend` is 20 | --| only called from a child of at least one `suspense` parent!* 21 | --| 22 | --| *Note: You probably shouldn't be using this function directly. It's 23 | --| primarily for library authors to build abstractions on top of, as 24 | --| it requires things like caching mechanisms external to the 25 | --| component tree.* 26 | --| 27 | --| *Warning: React's Suspense API is still experimental. It requires 28 | --| some manual setup as well as specific versions of React. The API 29 | --| is also not final and these functions may change.* 30 | suspend :: forall a. Suspended a -> Pure a 31 | suspend (Suspended e) = React.do 32 | unsafeRenderEffect do 33 | result <- e 34 | case result of 35 | InProgress fiber -> 36 | unsafeThrowPromise 37 | =<< Promise.fromAff (joinFiber fiber) 38 | Failed err -> throwError err 39 | Complete a -> pure a 40 | 41 | newtype Suspended a 42 | = Suspended (Effect (SuspenseResult a)) 43 | 44 | data SuspenseResult a 45 | = InProgress (Fiber a) 46 | | Failed Error 47 | | Complete a 48 | 49 | suspense :: { fallback :: JSX, children :: Array JSX } -> JSX 50 | suspense = element suspense_ 51 | 52 | foreign import suspense_ :: ReactComponent { children :: Array JSX, fallback :: JSX } 53 | 54 | --| Dangerously throw a `Promise` as though it were an `Error`. 55 | --| React's Suspense API catches thrown `Promise`s and suspends 56 | --| rendering until they complete. 57 | unsafeThrowPromise :: forall a. Promise a -> Effect a 58 | unsafeThrowPromise = throwError <<< (unsafeCoerce :: Promise a -> Error) 59 | -------------------------------------------------------------------------------- /src/React/Basic/Hooks/Suspense/Store.purs: -------------------------------------------------------------------------------- 1 | module React.Basic.Hooks.Suspense.Store 2 | ( mkSuspenseStore 3 | , SuspenseStore 4 | , get 5 | , get' 6 | ) where 7 | 8 | import Prelude 9 | import Control.Alt ((<|>)) 10 | import Data.DateTime.Instant (Instant, unInstant) 11 | import Data.Either (Either(..)) 12 | import Data.Int (ceil) 13 | import Data.Map (Map) 14 | import Data.Map as Map 15 | import Data.Maybe (Maybe(..)) 16 | import Data.Newtype (un) 17 | import Data.Time.Duration (Milliseconds(..)) 18 | import Effect (Effect) 19 | import Effect.Aff (Aff, attempt, launchAff, throwError) 20 | import Effect.Class (liftEffect) 21 | import Effect.Console (warn) 22 | import Effect.Exception (try) 23 | import Effect.Now (now) 24 | import Effect.Ref (Ref) 25 | import Effect.Ref as Ref 26 | import React.Basic.Hooks (type (/\), (/\)) 27 | import React.Basic.Hooks.Suspense (Suspended(..), SuspenseResult(..)) 28 | import Web.HTML (window) 29 | import Web.HTML.Window (requestIdleCallback) 30 | 31 | --| Simple key-based cache. 32 | mkSuspenseStore :: 33 | forall k v. 34 | Ord k => 35 | Maybe Milliseconds -> 36 | (k -> Aff v) -> 37 | Effect (SuspenseStore k v) 38 | mkSuspenseStore defaultMaxAge backend = do 39 | ref <- Ref.new Map.empty 40 | let 41 | isExpired maxAge now' (_ /\ savedTime) = unInstant savedTime <> maxAge < unInstant now' 42 | 43 | pruneCache = do 44 | case defaultMaxAge of 45 | Nothing -> pure unit 46 | Just maxAge -> do 47 | now' <- now 48 | void $ Ref.modify (Map.filter (not isExpired maxAge now')) ref 49 | void 50 | $ window 51 | >>= requestIdleCallback 52 | { timeout: ceil $ un Milliseconds maxAge 53 | } 54 | pruneCache 55 | 56 | tryFromCache itemMaxAge k = do 57 | rMaybe <- Map.lookup k <$> Ref.read ref 58 | case rMaybe of 59 | Nothing -> pure Nothing 60 | Just v@(r /\ _) -> do 61 | case itemMaxAge <|> defaultMaxAge of 62 | Nothing -> pure (Just r) 63 | Just maxAge -> do 64 | now' <- now 65 | if isExpired maxAge now' v then do 66 | _ <- Ref.modify (Map.delete k) ref 67 | pure Nothing 68 | else 69 | pure (Just r) 70 | 71 | getCacheOrBackend itemMaxAge k = do 72 | c <- tryFromCache itemMaxAge k 73 | case c of 74 | Just v -> pure v 75 | Nothing -> do 76 | fiber <- 77 | launchAff do 78 | r <- attempt do backend k 79 | liftEffect do 80 | let 81 | v = case r of 82 | Left e -> Failed e 83 | Right v' -> Complete v' 84 | d <- now 85 | _ <- 86 | ref 87 | # Ref.modify 88 | ( k 89 | # Map.alter case _ of 90 | Nothing -> Just (v /\ d) 91 | Just r'@(_ /\ d') -> 92 | if d > d' then 93 | Just (v /\ d) 94 | else 95 | Just r' 96 | ) 97 | case r of 98 | Left e -> throwError e 99 | Right v' -> pure v' 100 | let 101 | v = InProgress fiber 102 | d <- now 103 | _ <- ref # Ref.modify (Map.insert k (v /\ d)) 104 | pure v 105 | do 106 | r <- try pruneCache 107 | case r of 108 | Left _ -> warn "Failed to initialize the suspense store cleanup task. Ensure you're using it in a browser with `requestIdleCallback` support." 109 | Right _ -> pure unit 110 | pure 111 | $ SuspenseStore 112 | { cache: ref 113 | , get: map Suspended <<< getCacheOrBackend 114 | } 115 | 116 | newtype SuspenseStore k v 117 | = SuspenseStore 118 | { cache :: Ref (Map k (SuspenseResult v /\ Instant)) 119 | , get :: Maybe Milliseconds -> k -> Suspended v 120 | } 121 | 122 | get :: forall k v. SuspenseStore k v -> k -> Suspended v 123 | get (SuspenseStore s) = s.get Nothing 124 | 125 | get' :: forall k v. SuspenseStore k v -> Milliseconds -> k -> Suspended v 126 | get' (SuspenseStore s) d = s.get (Just d) 127 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Data.Time.Duration (Seconds(..), fromDuration) 7 | import Effect (Effect) 8 | import Effect.Aff (delay, launchAff_) 9 | import Test.Spec.Discovery (discover) 10 | import Test.Spec.Reporter (consoleReporter) 11 | import Test.Spec.Runner (defaultConfig, runSpec') 12 | 13 | main ∷ Effect Unit 14 | main = launchAff_ do 15 | specs <- discover "\\.*Spec" 16 | delay (1.0 # Seconds # fromDuration) 17 | runSpec' 18 | config 19 | [ consoleReporter ] 20 | specs 21 | where 22 | config = 23 | defaultConfig 24 | { slow = 5.0 # Seconds # fromDuration 25 | , timeout = Nothing 26 | } 27 | -------------------------------------------------------------------------------- /test/Spec/MemoSpec.purs: -------------------------------------------------------------------------------- 1 | module Test.Spec.MemoSpec where 2 | 3 | import Prelude 4 | 5 | import Data.Function (on) 6 | import Effect (Effect) 7 | import Effect.Class (class MonadEffect, liftEffect) 8 | import Effect.Ref (modify, new, read) 9 | import React.Basic.DOM as R 10 | import React.Basic.Hooks (ReactComponent, element, memo, memo', reactComponent, useEffectAlways) 11 | import React.Basic.Hooks as Hooks 12 | import React.TestingLibrary (cleanup, render) 13 | import Test.Spec (Spec, after_, before, describe, it) 14 | import Test.Spec.Assertions (shouldEqual) 15 | 16 | spec ∷ Spec Unit 17 | spec = 18 | after_ cleanup do 19 | before setup do 20 | describe "memo" do 21 | it "works with simple values" \{ memoTest } -> do 22 | rendersRef <- liftEffect do new 0 23 | let onRender = void $ modify (1 + _) rendersRef 24 | let renders = liftEffect do read rendersRef 25 | { rerender } <- render do element memoTest { onRender, arg: 0 } 26 | rerender do element memoTest { onRender, arg: 0 } 27 | renders >>= (_ `shouldEqual` 1) 28 | rerender do element memoTest { onRender, arg: 1 } 29 | renders >>= (_ `shouldEqual` 2) 30 | 31 | describe "memo'" do 32 | it "never renders if the eq fn returns true" \{ memo'TestAlwaysEq } -> do 33 | rendersRef <- liftEffect do new 0 34 | let onRender = void $ modify (1 + _) rendersRef 35 | let renders = liftEffect do read rendersRef 36 | { rerender } <- render do element memo'TestAlwaysEq { onRender, arg: 0 } 37 | rerender do element memo'TestAlwaysEq { onRender, arg: 0 } 38 | renders >>= (_ `shouldEqual` 1) 39 | rerender do element memo'TestAlwaysEq { onRender, arg: 1 } 40 | renders >>= (_ `shouldEqual` 1) 41 | 42 | it "always renders if the eq fn returns false" \{ memo'TestNeverEq } -> do 43 | rendersRef <- liftEffect do new 0 44 | let onRender = void $ modify (1 + _) rendersRef 45 | let renders = liftEffect do read rendersRef 46 | { rerender } <- render do element memo'TestNeverEq { onRender, arg: 0 } 47 | rerender do element memo'TestNeverEq { onRender, arg: 0 } 48 | renders >>= (_ `shouldEqual` 2) 49 | rerender do element memo'TestNeverEq { onRender, arg: 1 } 50 | renders >>= (_ `shouldEqual` 3) 51 | 52 | it "renders correctly over eq on props.arg" \{ memo'TestArgEq } -> do 53 | rendersRef <- liftEffect do new 0 54 | let onRender = void $ modify (1 + _) rendersRef 55 | let renders = liftEffect do read rendersRef 56 | { rerender } <- render do element memo'TestArgEq { onRender, arg: 0 } 57 | rerender do element memo'TestArgEq { onRender, arg: 0 } 58 | renders >>= (_ `shouldEqual` 1) 59 | rerender do element memo'TestArgEq { onRender, arg: 1 } 60 | renders >>= (_ `shouldEqual` 2) 61 | rerender do element memo'TestArgEq { onRender, arg: 1 } 62 | renders >>= (_ `shouldEqual` 2) 63 | where 64 | setup :: 65 | forall m. MonadEffect m => 66 | m 67 | { memoTest :: ReactComponent { onRender :: Effect Unit, arg :: Int } 68 | , memo'TestAlwaysEq :: ReactComponent { onRender :: Effect Unit, arg :: Int } 69 | , memo'TestNeverEq :: ReactComponent { onRender :: Effect Unit, arg :: Int } 70 | , memo'TestArgEq :: ReactComponent { onRender :: Effect Unit, arg :: Int } 71 | } 72 | setup = liftEffect do 73 | memoTest <- memo do 74 | reactComponent "MemoTest" \{ onRender } -> Hooks.do 75 | useEffectAlways do 76 | onRender 77 | pure (pure unit) 78 | pure $ R.div_ [] 79 | 80 | memo'TestAlwaysEq <- memo' (\_ _ -> true) do 81 | reactComponent "MemoTest" \{ onRender } -> Hooks.do 82 | useEffectAlways do 83 | onRender 84 | pure (pure unit) 85 | pure $ R.div_ [] 86 | 87 | memo'TestNeverEq <- memo' (\_ _ -> false) do 88 | reactComponent "MemoTest" \{ onRender } -> Hooks.do 89 | useEffectAlways do 90 | onRender 91 | pure (pure unit) 92 | pure $ R.div_ [] 93 | 94 | memo'TestArgEq <- memo' (eq `on` _.arg) do 95 | reactComponent "MemoTest" \{ onRender } -> Hooks.do 96 | useEffectAlways do 97 | onRender 98 | pure (pure unit) 99 | pure $ R.div_ [] 100 | 101 | pure 102 | { memoTest 103 | , memo'TestAlwaysEq 104 | , memo'TestNeverEq 105 | , memo'TestArgEq 106 | } -------------------------------------------------------------------------------- /test/Spec/React18HooksSpec.purs: -------------------------------------------------------------------------------- 1 | module Test.Spec.React18HooksSpec where 2 | 3 | import Prelude 4 | 5 | import Control.Monad.Rec.Class (forever) 6 | import Data.Array as Array 7 | import Data.Foldable (for_, traverse_) 8 | import Data.Maybe (fromMaybe) 9 | import Data.Monoid (guard, power) 10 | import Data.String as String 11 | import Data.Tuple.Nested ((/\)) 12 | import Effect.Aff (Milliseconds(..), apathize, delay, launchAff_) 13 | import Effect.Class (liftEffect) 14 | import Effect.Ref as Ref 15 | import Foreign.Object as Object 16 | import React.Basic (fragment) 17 | import React.Basic.DOM as R 18 | import React.Basic.DOM.Events (targetValue) 19 | import React.Basic.Events (handler, handler_) 20 | import React.Basic.Hooks (reactComponent) 21 | import React.Basic.Hooks as Hooks 22 | import React.TestingLibrary (cleanup, fireEventClick, renderComponent, typeText) 23 | import Test.Spec (Spec, after_, before, describe, it) 24 | import Test.Spec.Assertions (shouldNotEqual) 25 | import Test.Spec.Assertions.DOM (textContentShouldEqual) 26 | import Web.DOM.Element (getAttribute) 27 | import Web.HTML.HTMLElement as HTMLElement 28 | 29 | spec ∷ Spec Unit 30 | spec = 31 | after_ cleanup do 32 | before setup do 33 | describe "React 18 hooks" do 34 | it "useId works" \{ useId } -> do 35 | { findByTestId } <- renderComponent useId {} 36 | elem <- findByTestId "use-id" 37 | idʔ <- getAttribute "id" (HTMLElement.toElement elem) # liftEffect 38 | let id = idʔ # fromMaybe "" 39 | id `shouldNotEqual` "" 40 | elem `textContentShouldEqual` id 41 | 42 | it "useTransition works" \{ useTransition } -> do 43 | { findByText } <- renderComponent useTransition {} 44 | elem <- findByText "0" 45 | fireEventClick elem 46 | elem `textContentShouldEqual` "1" 47 | 48 | it "useDeferredValue hopefully works" \{ useDeferredValue } -> do 49 | { findByTestId } <- renderComponent useDeferredValue {} 50 | spanElem <- findByTestId "span" 51 | spanElem `textContentShouldEqual` "0" 52 | findByTestId "input" >>= typeText (power "text" 100) 53 | spanElem `textContentShouldEqual` "400" 54 | 55 | it "useSyncExternalStore" \{ useSyncExternalStore } -> do 56 | { findByTestId } <- renderComponent useSyncExternalStore {} 57 | spanElem <- findByTestId "span" 58 | spanElem `textContentShouldEqual` "0" 59 | delay (350.0 # Milliseconds) 60 | spanElem `textContentShouldEqual` "3" 61 | 62 | it "useInsertionEffect works" \{ useInsertionEffect } -> do 63 | { findByText } <- renderComponent useInsertionEffect {} 64 | void $ findByText "insertion-done" 65 | 66 | where 67 | setup = liftEffect ado 68 | 69 | useId <- 70 | reactComponent "UseIDExample" \(_ :: {}) -> Hooks.do 71 | id <- Hooks.useId 72 | pure $ R.div 73 | { id 74 | , _data: Object.singleton "testid" "use-id" 75 | , children: [ R.text id ] 76 | } 77 | 78 | useTransition <- 79 | reactComponent "UseTransitionExample" \(_ :: {}) -> Hooks.do 80 | isPending /\ startTransition <- Hooks.useTransition 81 | count /\ setCount <- Hooks.useState 0 82 | let handleClick = startTransition do setCount (_ + 1) 83 | pure $ R.div 84 | { children: 85 | [ guard isPending (R.text "Pending") 86 | , R.button 87 | { onClick: handler_ handleClick 88 | , children: [ R.text (show count) ] 89 | } 90 | ] 91 | } 92 | 93 | useDeferredValue <- 94 | reactComponent "UseDeferredValueExample" \(_ :: {}) -> Hooks.do 95 | text /\ setText <- Hooks.useState' "" 96 | textLength <- Hooks.useDeferredValue (String.length text) 97 | pure $ fragment 98 | [ R.input 99 | { onChange: handler targetValue (traverse_ setText) 100 | , _data: Object.singleton "testid" "input" 101 | } 102 | , R.span 103 | { _data: Object.singleton "testid" "span" 104 | , children: [ R.text (show textLength) ] 105 | } 106 | ] 107 | 108 | useInsertionEffect <- 109 | reactComponent "UseInsertionEffectExample" \(_ :: {}) -> Hooks.do 110 | text /\ setText <- Hooks.useState' "waiting" 111 | Hooks.useInsertionEffect unit do 112 | setText "insertion-done" 113 | mempty 114 | pure $ R.span_ [ R.text text ] 115 | 116 | useSyncExternalStore <- do 117 | { subscribe, getSnapshot, getServerSnapshot } <- do 118 | subscribersRef <- Ref.new [] 119 | intRef <- Ref.new 0 120 | -- Update the intRef every 100ms. 121 | launchAff_ $ apathize $ forever do 122 | delay (100.0 # Milliseconds) 123 | intRef # Ref.modify_ (_ + 1) # liftEffect 124 | subscribers <- subscribersRef # Ref.read # liftEffect 125 | liftEffect $ for_ subscribers identity 126 | 127 | pure 128 | { subscribe: \callback -> do 129 | subscribersRef # Ref.modify_ (Array.cons callback) 130 | pure $ 131 | subscribersRef # Ref.modify_ (Array.drop 1) 132 | , getSnapshot: Ref.read intRef 133 | , getServerSnapshot: Ref.read intRef 134 | } 135 | 136 | reactComponent "UseSyncExternalStoreExample" \(_ :: {}) -> Hooks.do 137 | number <- Hooks.useSyncExternalStore 138 | subscribe 139 | getSnapshot 140 | getServerSnapshot 141 | pure $ R.span { _data: Object.singleton "testid" "span", children: [ R.text (show number) ] } 142 | 143 | in { useId, useTransition, useDeferredValue, useInsertionEffect, useSyncExternalStore } -------------------------------------------------------------------------------- /test/Spec/UseReducerSpec.purs: -------------------------------------------------------------------------------- 1 | module Test.Spec.UseReducerSpec where 2 | 3 | import Prelude 4 | 5 | import Effect.Class (liftEffect) 6 | import React.Basic.DOM as R 7 | import React.Basic.DOM.Events (capture_) 8 | import React.Basic.Hooks (mkReducer, reactComponent, useReducer, (/\)) 9 | import React.Basic.Hooks as Hooks 10 | import React.TestingLibrary (cleanup, fireEventClick, renderComponent) 11 | import Test.Spec (Spec, after_, before, describe, it) 12 | import Test.Spec.Assertions.DOM (textContentShouldEqual) 13 | 14 | spec ∷ Spec Unit 15 | spec = 16 | after_ cleanup do 17 | before setup do 18 | describe "useReducer" do 19 | it "works with simple values" \{ useReducerWithInt } -> do 20 | { findByText } <- renderComponent useReducerWithInt {} 21 | elem <- findByText "0" 22 | fireEventClick elem 23 | elem `textContentShouldEqual` "1" 24 | 25 | it "works with function values" \{ useReducerWithFn } -> do 26 | -- this is not a normal way to use useReducer, but it 27 | -- still shouldn't break at runtime 28 | { findByText } <- renderComponent useReducerWithFn {} 29 | elem <- findByText "0" 30 | fireEventClick elem 31 | elem `textContentShouldEqual` "1" 32 | 33 | where 34 | setup = liftEffect do 35 | 36 | useReducerWithInt <- do 37 | reducer <- mkReducer \count -> case _ of 38 | Add -> count + 1 39 | Subtract -> count - 1 40 | 41 | reactComponent "Counter" \_ -> Hooks.do 42 | count /\ dispatch <- useReducer 0 reducer 43 | pure $ R.button 44 | { onClick: capture_ do 45 | dispatch Add 46 | , children: [ R.text $ show count ] 47 | } 48 | 49 | useReducerWithFn <- do 50 | reducer <- mkReducer \countFn -> case _ of 51 | Add -> \_ -> countFn unit + 1 52 | Subtract -> \_ -> countFn unit - 1 53 | 54 | reactComponent "Counter" \_ -> Hooks.do 55 | count /\ dispatch <- useReducer (\_ -> 0) reducer 56 | pure $ R.button 57 | { onClick: capture_ do 58 | dispatch Add 59 | , children: [ R.text $ show $ count unit ] 60 | } 61 | 62 | pure 63 | { useReducerWithInt 64 | , useReducerWithFn 65 | } 66 | 67 | data Action = Add | Subtract -------------------------------------------------------------------------------- /test/Spec/UseStateSpec.purs: -------------------------------------------------------------------------------- 1 | module Test.Spec.UseStateSpec where 2 | 3 | import Prelude 4 | 5 | import Effect.Class (liftEffect) 6 | import React.Basic.DOM as R 7 | import React.Basic.DOM.Events (capture_) 8 | import React.Basic.Hooks (reactComponent, useState, useState', (/\)) 9 | import React.Basic.Hooks as Hooks 10 | import React.TestingLibrary (cleanup, fireEventClick, renderComponent) 11 | import Test.Spec (Spec, after_, before, describe, it) 12 | import Test.Spec.Assertions.DOM (textContentShouldEqual) 13 | 14 | 15 | spec ∷ Spec Unit 16 | spec = 17 | after_ cleanup do 18 | before setup do 19 | describe "useState" do 20 | it "works with simple values" \{ useStateWithInt } -> do 21 | { findByText } <- renderComponent useStateWithInt {} 22 | elem <- findByText "0" 23 | fireEventClick elem 24 | elem `textContentShouldEqual` "1" 25 | 26 | it "works with function values" \{ useStateWithFn } -> do 27 | -- this is not a normal way to use useState, but it 28 | -- still shouldn't break at runtime 29 | { findByText } <- renderComponent useStateWithFn {} 30 | elem <- findByText "0" 31 | fireEventClick elem 32 | elem `textContentShouldEqual` "1" 33 | 34 | describe "useState'" do 35 | it "works with simple values" \{ useState'WithInt } -> do 36 | { findByText } <- renderComponent useState'WithInt {} 37 | elem <- findByText "0" 38 | fireEventClick elem 39 | elem `textContentShouldEqual` "1" 40 | 41 | it "works with function values" \{ useState'WithFn } -> do 42 | -- this is not a normal way to use useState', but it 43 | -- still shouldn't break at runtime 44 | { findByText } <- renderComponent useState'WithFn {} 45 | elem <- findByText "0" 46 | fireEventClick elem 47 | elem `textContentShouldEqual` "1" 48 | 49 | where 50 | setup = liftEffect do 51 | useStateWithInt <- 52 | reactComponent "Counter" \_ -> Hooks.do 53 | count /\ setCount <- useState 0 54 | pure $ R.button 55 | { onClick: capture_ do 56 | setCount (_ + 1) 57 | , children: [ R.text $ show count ] 58 | } 59 | 60 | useStateWithFn <- 61 | reactComponent "Counter" \_ -> Hooks.do 62 | count /\ setCount <- useState (\_ -> 0) 63 | pure $ R.button 64 | { onClick: capture_ do 65 | setCount (\fn -> \_ -> fn unit + 1) 66 | , children: [ R.text $ show $ count unit ] 67 | } 68 | 69 | useState'WithInt <- 70 | reactComponent "Counter" \_ -> Hooks.do 71 | count /\ setCount <- useState' 0 72 | pure $ R.button 73 | { onClick: capture_ do 74 | setCount $ count + 1 75 | , children: [ R.text $ show count ] 76 | } 77 | 78 | useState'WithFn <- 79 | reactComponent "Counter" \_ -> Hooks.do 80 | count /\ setCount <- useState' (\_ -> 0) 81 | pure $ R.button 82 | { onClick: capture_ do 83 | setCount \_ -> count unit + 1 84 | , children: [ R.text $ show $ count unit ] 85 | } 86 | 87 | pure 88 | { useStateWithInt 89 | , useStateWithFn 90 | , useState'WithInt 91 | , useState'WithFn 92 | } --------------------------------------------------------------------------------