├── .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 [](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 | }
--------------------------------------------------------------------------------