├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── test └── Main.purs ├── src ├── javascript │ ├── Counter.js │ └── App.js ├── index.js └── purescript │ ├── App.purs │ ├── Counter │ └── Interop.purs │ └── Counter.purs ├── spago.dhall ├── .gitignore ├── README.md ├── package.json └── packages.dhall /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pete-murphy/purescript-react-tutorial/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pete-murphy/purescript-react-tutorial/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pete-murphy/purescript-react-tutorial/HEAD/public/logo512.png -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Class.Console (log) 7 | 8 | main :: Effect Unit 9 | main = do 10 | log "🍝" 11 | log "You should add some tests." 12 | -------------------------------------------------------------------------------- /src/javascript/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | export default function Counter({ label }) { 4 | const [count, setCount] = useState(0); 5 | return ( 6 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | /* Uncomment the following line to use the `./purescript/App` version of the App */ 4 | // import { app as App } from "./output/App"; 5 | import App from "./javascript/App"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | -------------------------------------------------------------------------------- /spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "react-tutorial" 6 | , dependencies = 7 | [ "console" 8 | , "effect" 9 | , "generics-rep" 10 | , "psci-support" 11 | , "react-basic-dom" 12 | , "react-basic-hooks" 13 | , "read" 14 | ] 15 | , packages = ./packages.dhall 16 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 17 | } 18 | -------------------------------------------------------------------------------- /src/javascript/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { jsCounter as Counter } from "../output/Counter.Interop"; 3 | 4 | function App() { 5 | return ( 6 |
7 |

My JavaScript App

8 | console.log("Clicked", n)} /> 9 | 10 | 11 |
12 | ); 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # purescript 26 | output 27 | .psc* 28 | .purs* 29 | .spago 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to Replace React Components With PureScript's React Libraries 2 | 3 | Working through this blog post by Thomas Honeyman: https://thomashoneyman.com/articles/replace-react-components-with-purescript/. 4 | 5 | ### Usage 6 | 7 | - Install dependencies with 8 | 9 | ```shell 10 | $ yarn; yarn spago build 11 | ``` 12 | 13 | - Make a symbolic link for PureScript output folder so Create React App webpack server can access it. 14 | 15 | ``` 16 | $ ln -s $PWD/output $PWD/src/output 17 | ``` 18 | 19 | - Run the development server with `yarn start` 20 | -------------------------------------------------------------------------------- /src/purescript/App.purs: -------------------------------------------------------------------------------- 1 | module App where 2 | 3 | import Prelude 4 | import Counter (CounterType(..), mkCounter) 5 | import React.Basic.DOM as R 6 | import React.Basic.Hooks (Component) 7 | import React.Basic.Hooks as React 8 | 9 | mkApp :: Component Unit 10 | mkApp = do 11 | counter <- mkCounter 12 | React.component "App" \_ -> React.do 13 | pure do 14 | R.div_ 15 | [ R.h1_ [ R.text "My App" ] 16 | , counter 17 | { counterType: Increment 18 | , label: "Count" 19 | , onClick: mempty 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purecript-react-tutorial", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^17.0.1", 7 | "react-dom": "^17.0.1", 8 | "react-scripts": "4.0.2" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "eject": "react-scripts eject" 14 | }, 15 | "eslintConfig": { 16 | "extends": "react-app" 17 | }, 18 | "browserslist": { 19 | "production": [ 20 | ">0.2%", 21 | "not dead", 22 | "not op_mini all" 23 | ], 24 | "development": [ 25 | "last 1 chrome version", 26 | "last 1 firefox version", 27 | "last 1 safari version" 28 | ] 29 | }, 30 | "devDependencies": { 31 | "purescript": "^0.13.8", 32 | "spago": "^0.19.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/purescript/Counter/Interop.purs: -------------------------------------------------------------------------------- 1 | module Counter.Interop where 2 | 3 | import Prelude 4 | import Counter (CounterType(..), Props, renderCounter) 5 | import Data.Maybe as Maybe 6 | import Data.Nullable (Nullable) 7 | import Data.Nullable as Nullable 8 | import Data.String.Read as Read 9 | import Effect.Uncurried (EffectFn1) 10 | import Effect.Uncurried as Uncurried 11 | import Effect.Unsafe as Unsafe 12 | import React.Basic.Hooks (JSX) 13 | import React.Basic.Hooks as React 14 | 15 | type JSProps 16 | = { label :: Nullable String 17 | , onClick :: Nullable (EffectFn1 Int Unit) 18 | , counterType :: Nullable String 19 | } 20 | 21 | jsPropsToProps :: JSProps -> Props 22 | jsPropsToProps props = 23 | { label: 24 | Maybe.fromMaybe (deriveLabelFromType props.counterType) (Nullable.toMaybe props.label) 25 | , onClick: 26 | Maybe.fromMaybe mempty (map Uncurried.runEffectFn1 (Nullable.toMaybe props.onClick)) 27 | , counterType: 28 | Maybe.fromMaybe Increment (Read.read =<< Nullable.toMaybe props.counterType) 29 | } 30 | where 31 | deriveLabelFromType :: Nullable String -> String 32 | deriveLabelFromType = Maybe.fromMaybe "Count" <<< Nullable.toMaybe 33 | 34 | jsCounter :: JSProps -> JSX 35 | jsCounter = 36 | Unsafe.unsafePerformEffect do 37 | React.component "Counter" (renderCounter <<< jsPropsToProps) 38 | -------------------------------------------------------------------------------- /src/purescript/Counter.purs: -------------------------------------------------------------------------------- 1 | module Counter where 2 | 3 | import Prelude 4 | import Data.Generic.Rep (class Generic) 5 | import Data.Generic.Rep.Show (genericShow) 6 | import Data.Maybe (Maybe(..)) 7 | import Data.String.Read (class Read) 8 | import Effect (Effect) 9 | import React.Basic.DOM as R 10 | import React.Basic.Events as Events 11 | import React.Basic.Hooks (Component, JSX, Render, UseState, (/\)) 12 | import React.Basic.Hooks as React 13 | 14 | type Props 15 | = { label :: String 16 | , onClick :: Int -> Effect Unit 17 | , counterType :: CounterType 18 | } 19 | 20 | data CounterType 21 | = Increment 22 | | Decrement 23 | 24 | derive instance genericCounterType :: Generic CounterType _ 25 | 26 | instance showCounterType :: Show CounterType where 27 | show = genericShow 28 | 29 | instance readCounterType :: Read CounterType where 30 | read = case _ of 31 | "Increment" -> Just Increment 32 | "Decrement" -> Just Decrement 33 | _ -> Nothing 34 | 35 | mkCounter :: Component Props 36 | mkCounter = React.component "Counter" renderCounter 37 | 38 | renderCounter :: Props -> Render Unit (UseState Int Unit) JSX 39 | renderCounter props = React.do 40 | count /\ setCount <- React.useState' 0 41 | pure do 42 | R.button 43 | { onClick: 44 | Events.handler_ do 45 | let 46 | newCount = case props.counterType of 47 | Increment -> count + 1 48 | Decrement -> count - 1 49 | setCount newCount 50 | props.onClick newCount 51 | , children: 52 | [ R.text (props.label <> " " <> show count) ] 53 | } 54 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to your new Dhall package-set! 3 | 4 | Below are instructions for how to edit this file for most use 5 | cases, so that you don't need to know Dhall to use it. 6 | 7 | ## Warning: Don't Move This Top-Level Comment! 8 | 9 | Due to how `dhall format` currently works, this comment's 10 | instructions cannot appear near corresponding sections below 11 | because `dhall format` will delete the comment. However, 12 | it will not delete a top-level comment like this one. 13 | 14 | ## Use Cases 15 | 16 | Most will want to do one or both of these options: 17 | 1. Override/Patch a package's dependency 18 | 2. Add a package not already in the default package set 19 | 20 | This file will continue to work whether you use one or both options. 21 | Instructions for each option are explained below. 22 | 23 | ### Overriding/Patching a package 24 | 25 | Purpose: 26 | - Change a package's dependency to a newer/older release than the 27 | default package set's release 28 | - Use your own modified version of some dependency that may 29 | include new API, changed API, removed API by 30 | using your custom git repo of the library rather than 31 | the package set's repo 32 | 33 | Syntax: 34 | where `entityName` is one of the following: 35 | - dependencies 36 | - repo 37 | - version 38 | ------------------------------- 39 | let upstream = -- 40 | in upstream 41 | with packageName.entityName = "new value" 42 | ------------------------------- 43 | 44 | Example: 45 | ------------------------------- 46 | let upstream = -- 47 | in upstream 48 | with halogen.version = "master" 49 | with halogen.repo = "https://example.com/path/to/git/repo.git" 50 | 51 | with halogen-vdom.version = "v4.0.0" 52 | ------------------------------- 53 | 54 | ### Additions 55 | 56 | Purpose: 57 | - Add packages that aren't already included in the default package set 58 | 59 | Syntax: 60 | where `` is: 61 | - a tag (i.e. "v4.0.0") 62 | - a branch (i.e. "master") 63 | - commit hash (i.e. "701f3e44aafb1a6459281714858fadf2c4c2a977") 64 | ------------------------------- 65 | let upstream = -- 66 | in upstream 67 | with new-package-name = 68 | { dependencies = 69 | [ "dependency1" 70 | , "dependency2" 71 | ] 72 | , repo = 73 | "https://example.com/path/to/git/repo.git" 74 | , version = 75 | "" 76 | } 77 | ------------------------------- 78 | 79 | Example: 80 | ------------------------------- 81 | let upstream = -- 82 | in upstream 83 | with benchotron = 84 | { dependencies = 85 | [ "arrays" 86 | , "exists" 87 | , "profunctor" 88 | , "strings" 89 | , "quickcheck" 90 | , "lcg" 91 | , "transformers" 92 | , "foldable-traversable" 93 | , "exceptions" 94 | , "node-fs" 95 | , "node-buffer" 96 | , "node-readline" 97 | , "datetime" 98 | , "now" 99 | ] 100 | , repo = 101 | "https://github.com/hdgarrood/purescript-benchotron.git" 102 | , version = 103 | "v7.0.0" 104 | } 105 | ------------------------------- 106 | -} 107 | let upstream = 108 | https://github.com/purescript/package-sets/releases/download/psc-0.13.8-20210118/packages.dhall sha256:a59c5c93a68d5d066f3815a89f398bcf00e130a51cb185b2da29b20e2d8ae115 109 | 110 | in upstream 111 | --------------------------------------------------------------------------------