diff --git a/examples/async/.gitignore b/examples/async/.gitignore
new file mode 100644
index 0000000..645684d
--- /dev/null
+++ b/examples/async/.gitignore
@@ -0,0 +1,4 @@
+output
+html/index.js
+package-lock.json
+node_modules
diff --git a/examples/async/Makefile b/examples/async/Makefile
new file mode 100644
index 0000000..ecacfbe
--- /dev/null
+++ b/examples/async/Makefile
@@ -0,0 +1,8 @@
+all: node_modules
+ purs compile src/*.purs '../../src/**/*.purs' '../../bower_components/purescript-*/src/**/*.purs'
+ purs bundle -m Main --main Main output/*/*.js > output/bundle.js
+ node_modules/.bin/browserify output/bundle.js -o html/index.js
+
+node_modules:
+ npm install
+
diff --git a/examples/async/README.md b/examples/async/README.md
new file mode 100644
index 0000000..8d2e1b8
--- /dev/null
+++ b/examples/async/README.md
@@ -0,0 +1,12 @@
+# Async Counter Example
+
+## Building
+
+```
+npm install
+make all
+```
+
+This will compile the PureScript source files, bundle them, and use Browserify to combine PureScript and NPM sources into a single bundle.
+
+Then open `html/index.html` in your browser.
diff --git a/examples/async/html/index.html b/examples/async/html/index.html
new file mode 100644
index 0000000..6b93b7c
--- /dev/null
+++ b/examples/async/html/index.html
@@ -0,0 +1,10 @@
+
+
+
+ react-basic example
+
+
+
+
+
+
diff --git a/examples/async/package.json b/examples/async/package.json
new file mode 100644
index 0000000..583c22b
--- /dev/null
+++ b/examples/async/package.json
@@ -0,0 +1,9 @@
+{
+ "dependencies": {
+ "react": "16.6.0",
+ "react-dom": "16.6.0"
+ },
+ "devDependencies": {
+ "browserify": "16.2.3"
+ }
+}
diff --git a/examples/async/src/AsyncCounter.purs b/examples/async/src/AsyncCounter.purs
new file mode 100644
index 0000000..ff0be72
--- /dev/null
+++ b/examples/async/src/AsyncCounter.purs
@@ -0,0 +1,51 @@
+module AsyncCounter where
+
+import Prelude
+
+import Effect.Aff (Milliseconds(..), delay)
+import Effect.Class (liftEffect)
+import Effect.Console (log)
+import React.Basic (Component, JSX, StateUpdate(..), capture_, createComponent, fragment, keyed, make)
+import React.Basic.Components.Async (asyncWithLoader)
+import React.Basic.DOM as R
+
+component :: Component Props
+component = createComponent "AsyncCounter"
+
+type Props =
+ { label :: String
+ }
+
+data Action
+ = Increment
+
+asyncCounter :: Props -> JSX
+asyncCounter = make component { initialState, update, render }
+ where
+ initialState = { counter: 0 }
+
+ update self = case _ of
+ Increment ->
+ Update self.state { counter = self.state.counter + 1 }
+
+ render self =
+ fragment
+ [ R.p_ [ R.text "Notes:" ]
+ , R.ol_
+ [ R.li_ [ R.text "The two counts should never be out of sync" ]
+ , R.li_ [ R.text "\"done\" should only be logged to the console once for any loading period (in-flight requests get cancelled as the next request starts)" ]
+ ]
+ , R.button
+ { onClick: capture_ self Increment
+ , children: [ R.text (self.props.label <> ": " <> show self.state.counter) ]
+ }
+ , R.text " "
+ , keyed (show self.state.counter) $
+ asyncWithLoader (R.text "Loading...") do
+ liftEffect $ log "start"
+ delay $ Milliseconds 2000.0
+ liftEffect $ log "done"
+ pure $ R.text $ "Done: " <> show self.state.counter
+ ]
+
+
diff --git a/examples/async/src/Main.purs b/examples/async/src/Main.purs
new file mode 100644
index 0000000..08480c7
--- /dev/null
+++ b/examples/async/src/Main.purs
@@ -0,0 +1,22 @@
+module Main where
+
+import Prelude
+
+import AsyncCounter (asyncCounter)
+import Data.Maybe (Maybe(..))
+import Effect (Effect)
+import Effect.Exception (throw)
+import React.Basic.DOM (render)
+import Web.DOM.NonElementParentNode (getElementById)
+import Web.HTML (window)
+import Web.HTML.HTMLDocument (toNonElementParentNode)
+import Web.HTML.Window (document)
+
+main :: Effect Unit
+main = do
+ container <- getElementById "container" =<< (map toNonElementParentNode $ document =<< window)
+ case container of
+ Nothing -> throw "Container element not found."
+ Just c ->
+ let app = asyncCounter { label: "Async Increment" }
+ in render app c
diff --git a/src/React/Basic.purs b/src/React/Basic.purs
index 3dd0715..4b247c2 100644
--- a/src/React/Basic.purs
+++ b/src/React/Basic.purs
@@ -146,10 +146,6 @@ foreign import createComponent
. String
-> Component props
--- | A simplified alias for `ComponentSpec`. This type is usually used to represent
--- | the default component type returned from `createComponent`.
--- type Component props = forall state action. ComponentSpec props state action
-
-- | Opaque component information for internal use.
-- |
-- | __*Note:* Never define a component with
@@ -291,7 +287,7 @@ foreign import make
:: forall spec spec_ props state action
. Union spec spec_ (ComponentSpec props state action)
=> Component props
- -> { render :: Self props state action -> JSX | spec }
+ -> { initialState :: state, render :: Self props state action -> JSX | spec }
-> props
-> JSX
@@ -318,7 +314,7 @@ makeStateless
-> props
-> JSX
makeStateless component render =
- make component { render: \self -> render self.props }
+ make component { initialState: unit, render: \self -> render self.props }
-- | Represents rendered React VDOM (the result of calling `React.createElement`
-- | in JavaScript).
diff --git a/src/React/Basic/Components/Async.purs b/src/React/Basic/Components/Async.purs
new file mode 100644
index 0000000..17f3991
--- /dev/null
+++ b/src/React/Basic/Components/Async.purs
@@ -0,0 +1,64 @@
+module React.Basic.Components.Async
+ ( async
+ , asyncWithLoader
+ ) where
+
+import Prelude
+
+import Data.Maybe (Maybe(..), fromMaybe)
+import Effect.Aff (Aff, Fiber, error, killFiber, launchAff, launchAff_)
+import Effect.Class (liftEffect)
+import React.Basic (Component, JSX, StateUpdate(..), createComponent, empty, make, send)
+
+component :: Component (Aff JSX)
+component = createComponent "Async"
+
+data FetchAction
+ = ReplaceFiber (Fiber Unit)
+ | UpdateJSX JSX
+
+async :: Aff JSX -> JSX
+async = asyncWithLoader empty
+
+asyncWithLoader :: JSX -> Aff JSX -> JSX
+asyncWithLoader loader = make component
+ { initialState
+ , update
+ , render
+ , didMount: launch
+ -- , didUpdate: No! Implementing `didUpdate` breaks the
+ -- Aff/Component lifecycle relationship.
+ -- To update the Aff over time, wrap this
+ -- component with `keyed`.
+ , willUnmount: cleanup
+ }
+ where
+ initialState =
+ { jsx: Nothing
+ , pendingFiber: pure unit
+ }
+
+ update { props, state } = case _ of
+ ReplaceFiber newFiber ->
+ UpdateAndSideEffects
+ state { jsx = Nothing, pendingFiber = newFiber }
+ \_ -> kill state.pendingFiber
+
+ UpdateJSX jsx ->
+ Update
+ state { jsx = Just jsx }
+
+ render self =
+ fromMaybe loader self.state.jsx
+
+ launch self = do
+ fiber <- launchAff do
+ jsx <- self.props
+ liftEffect $ send self $ UpdateJSX jsx
+ send self $ ReplaceFiber fiber
+
+ cleanup self =
+ kill self.state.pendingFiber
+
+ kill =
+ launchAff_ <<< killFiber (error "Cancelled")