Skip to content

Aff refactor and Suspense experiments #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/bower_components/
/node_modules/
/.spago/
/.pulp-cache/
/output/
/generated-docs/
Expand Down
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"trailingComma": "none"
}
30 changes: 21 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
all: build examples
TOPTARGETS := all clean

build: bower_components node_modules
npx pulp build
EXAMPLES := $(wildcard examples/*/.)

examples: bower_components node_modules
find examples -maxdepth 2 -type f -iname makefile -execdir make \;
all: build $(EXAMPLES)

bower_components: node_modules
npx bower --allow-root install
build: output

output: .spago node_modules
npx spago build

.spago: node_modules
npx spago install

node_modules:
npm i --no-save bower pulp purescript
npm i --no-save spago purescript bower pulp

clean: $(EXAMPLES)
rm -rf node_modules bower_components .spago output

$(EXAMPLES):
cd $@ && $(MAKE) $(MAKECMDGOALS)

serve:
npx serve

.PHONY: build examples
.PHONY: build clean $(TOPTARGETS) $(EXAMPLES) serve
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ mkCounter = do

More examples:

- [Counter with an effect](./examples/counter/src/Counter.purs)
- [Reducer/action-style](./examples/reducer/src/Reducer.purs)
- [Controlled inputs](./examples/controlled-input/src/ControlledInput.purs)
- Components: [Parent](./examples/component/src/Container.purs) and [Child](./examples/component/src/ToggleButton.purs)
- [Refs to DOM nodes](./examples/refs/src/Refs.purs) (and extracting hook logic from a component for reuse)
- [A Todo App](./examples/todo-app/src/TodoApp.purs) (components, inputs, state)
- [Context](./examples/context/src/Context.purs) (creating and consuming React context)
- [Aff helper](./examples/aff/src/AffEx.purs) (async state management)
- [Counter with an effect](./examples/counter/src/Example.purs)
- [Reducer/action-style](./examples/reducer/src/Example.purs)
- [Controlled inputs](./examples/controlled-input/src/Example.purs)
- Components: [Parent](./examples/component/src/Example.purs) and [Child](./examples/component/src/ToggleButton.purs)
- [Refs to DOM nodes](./examples/refs/src/Example.purs) (and extracting hook logic from a component for reuse)
- [A Todo App](./examples/todo-app/src/Example.purs) (components, inputs, state)
- [Context](./examples/context/src/Example.purs) (creating and consuming React context)
- [Aff](./examples/aff/src/Example.purs) (rendering async data, using error boundaries)
- [Suspense](./examples/suspense/src/Example.purs) (experimental, React Suspense demo -- similar to the Aff example, but the loading state is managed by the parent instead of the detail rendering component)
11 changes: 11 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Examples

## Building an example

In the example folder:

```sh
make
```

Then open `html/index.html` in your browser.
21 changes: 16 additions & 5 deletions examples/aff/Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
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
parent_sources := $$(npx spago -x ../../spago.dhall sources | sed 's/^/..\/..\//' | tr '\n' ' ')

all: output node_modules
npx purs bundle -m Main --main Main output/*/*.js > output/bundle.js
npx browserify output/bundle.js -o html/index.js

output: node_modules
set -o noglob && \
npx purs compile \
src/**/*.purs \
$(parent_sources)

node_modules:
npm install
npm i --no-save browserify react react-dom

clean:
rm -rf node_modules output

.PHONY: all output clean
11 changes: 0 additions & 11 deletions examples/aff/README.md

This file was deleted.

9 changes: 0 additions & 9 deletions examples/aff/package.json

This file was deleted.

109 changes: 0 additions & 109 deletions examples/aff/src/AffEx.purs

This file was deleted.

142 changes: 142 additions & 0 deletions examples/aff/src/Example.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
module Example where

import Prelude
import Data.Foldable (find)
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype, un)
import Effect.Aff (Aff, Milliseconds(..), delay, error, message, throwError)
import React.Basic.DOM as R
import React.Basic.DOM.Events (capture_)
import React.Basic.Events (handler_)
import React.Basic.Hooks (Component, component, fragment, useState, (/\))
import React.Basic.Hooks as React
import React.Basic.Hooks.Aff (useAff)
import React.Basic.Hooks.ErrorBoundary (mkErrorBoundary)

mkExample :: Component Unit
mkExample = do
errorBoundary <- mkErrorBoundary "AffExErrorBoundary"
catDetails <- mkCatDetails
component "AffEx" \props -> React.do
catKey /\ setCatKey <- useState Nothing
let
reset = setCatKey \_ -> Nothing
pure
$ R.div_
[ R.h2_ [ R.text "Cat chooser" ]
, errorBoundary \{ error, dismissError } -> case error of
Just e -> renderAppError e (reset *> dismissError)
Nothing ->
fragment
[ catKeyList catKey setCatKey
, case catKey of
Nothing -> mempty
Just k -> catDetails k
]
]
where
-- This component is the main `useAff` demo. It receives a key
-- as a prop and renders both the loading state and the final
-- result.
mkCatDetails :: Component (Key Cat)
mkCatDetails = do
component "CatDetails" \catKey -> React.do
catState <- useAff catKey $ fetch catKey
pure
$ R.p_
[ case map entity catState of
Nothing -> R.text "Loading..."
Just (Cat { name }) -> R.text $ "A cat named " <> name
]

renderAppError error resetApp =
fragment
[ R.p_ [ R.text "Error!" ]
, R.p_ [ R.text $ message error ]
, R.button
{ onClick: capture_ do resetApp
, children: [ R.text "Reset" ]
}
]

catKeyList selectedCatKey setCatKey =
let
cats =
fakeDb
<> [ Entity
(Key "error (choose to throw a React render error)")
(Cat { name: "" })
]

catKeyRadioButton k =
R.div_
[ R.label_
[ R.input
{ type: "radio"
, name: "cat-key"
, checked: Just k == selectedCatKey
, onChange:
handler_ do
setCatKey \_ -> Just k
}
, R.text $ " Cat " <> un Key k
]
]
in
fragment $ map (catKeyRadioButton <<< key) cats

--
-- The bits below this point aren't directly relevant to the example,
-- just a slightly more interesting data model than returing a single
-- string.
--
--
--
-- Typed keys are a great way to tie entity-specific behavior
-- to an ID. We can use this phantom type to write a class
-- for generic, type-safe data fetching.
newtype Key entity
= Key String

derive instance eqKey :: Eq (Key entity)

derive instance ntKey :: Newtype (Key entity) _

-- An entity wrapper. In a real app this would hold other metadata
-- such as create and update dates.
data Entity entity
= Entity (Key entity) entity

key :: forall entity. Entity entity -> Key entity
key (Entity k _) = k

entity :: forall entity. Entity entity -> entity
entity (Entity _ e) = e

class Fetch entity where
fetch :: Key entity -> Aff (Entity entity)

-- An example entity
newtype Cat
= Cat { name :: String }

fakeDb :: Array (Entity Cat)
fakeDb =
[ Entity (Key "abc") (Cat { name: "Herb" })
, Entity (Key "def") (Cat { name: "Maxi" })
, Entity (Key "ghi") (Cat { name: "Chloe" })
]

instance fetchCat :: Fetch Cat where
fetch k = do
delay $ Milliseconds 300.0
-- pretend this happens on the server
case fakeDb # find (key >>> (_ == k)) of
Nothing ->
-- This should never happen in a normal application path
-- if only the server can generate keys :)
throwError
$ error
$ "DB error: Cat not found for key "
<> un Key k
Just e -> pure e
Loading