From f889de1bfc107015a419d6c64fa551a07c66dbe0 Mon Sep 17 00:00:00 2001 From: katsujukou <74475348+katsujukou@users.noreply.github.com> Date: Sun, 8 Aug 2021 06:57:39 +0900 Subject: [PATCH] Add UseSelector hook (#3) --- README.md | 20 ++++++++++++++ example/basic-hooks/Basic/Counter.purs | 30 ++++++++++++++++++++ example/basic-hooks/Basic/Main.purs | 17 ++++++++++++ example/basic-hooks/Basic/Store.purs | 17 ++++++++++++ example/basic-hooks/README.md | 3 ++ example/basic-hooks/index.html | 17 ++++++++++++ package.json | 3 +- spago.dhall | 2 ++ src/Halogen/Store/Hooks/UseSelector.purs | 35 ++++++++++++++++++++++++ src/Halogen/Store/Monad.purs | 6 ++++ 10 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 example/basic-hooks/Basic/Counter.purs create mode 100644 example/basic-hooks/Basic/Main.purs create mode 100644 example/basic-hooks/Basic/Store.purs create mode 100644 example/basic-hooks/README.md create mode 100644 example/basic-hooks/index.html create mode 100644 src/Halogen/Store/Hooks/UseSelector.purs diff --git a/README.md b/README.md index 01dcca7..2320c30 100644 --- a/README.md +++ b/README.md @@ -198,3 +198,23 @@ main = launchAff_ do root <- runStoreT BS.initialStore BS.reduce Counter.component runUI root unit body ``` + +### Using store with hooks + +If you want to write your component with [Halogen Hooks library](https://github.com/thomashoneyman/purescript-halogen-hooks) ,then you can use `useSelector` hook to access store. It takes selector and return the part of current store retrieved via given selector. + +```purs +import Halogen.Hooks as Hooks +import Halogen.Store.Hooks (useSelector) +import Halogen.Store.Select (selectAll) + +component :: forall q i o m + . MonadStore BS.Action BS.Store m + => H.Component q i o m +component = Hooks.component \_ _ -> Hooks.do + ctx <- useSelector selectAll + Hooks.pure do + ... +``` + +Unlike the case with connect, though, context returned by `useSelector` hook has type `Maybe store`, because the hook does not have access to store before it has been initialized. \ No newline at end of file diff --git a/example/basic-hooks/Basic/Counter.purs b/example/basic-hooks/Basic/Counter.purs new file mode 100644 index 0000000..90acf51 --- /dev/null +++ b/example/basic-hooks/Basic/Counter.purs @@ -0,0 +1,30 @@ +module Hooks.Counter where + +import Prelude + +import Basic.Store as BS +import Data.Maybe (fromMaybe) +import Halogen as H +import Halogen.HTML as HH +import Halogen.HTML.Events as HE +import Halogen.Hooks as Hooks +import Halogen.Store.Hooks.UseSelector (useSelector) +import Halogen.Store.Monad (class MonadStore, updateStore) +import Halogen.Store.Select (selectEq) + +component :: forall q i o m + . MonadStore BS.Action BS.Store m + => H.Component q i o m +component = Hooks.component \_ _ -> Hooks.do + count <- useSelector $ selectEq _.count + Hooks.pure do + let cnt = fromMaybe 0 count + HH.div_ + [ HH.button + [ HE.onClick \_ -> updateStore BS.Increment ] + [ HH.text "Increment"] + , HH.text $ " Count: " <> show cnt <> " " + , HH.button + [ HE.onClick \_ -> updateStore BS.Decrement ] + [ HH.text "Decrement" ] + ] \ No newline at end of file diff --git a/example/basic-hooks/Basic/Main.purs b/example/basic-hooks/Basic/Main.purs new file mode 100644 index 0000000..293ac82 --- /dev/null +++ b/example/basic-hooks/Basic/Main.purs @@ -0,0 +1,17 @@ +module Hooks.Main where + +import Prelude + +import Basic.Counter as Counter +import Basic.Store as BS +import Effect (Effect) +import Effect.Aff (launchAff_) +import Halogen.Aff as HA +import Halogen.Store.Monad (runStoreT) +import Halogen.VDom.Driver (runUI) + +main :: Effect Unit +main = launchAff_ do + body <- HA.awaitBody + root <- runStoreT BS.initialStore BS.reduce Counter.component + runUI root unit body diff --git a/example/basic-hooks/Basic/Store.purs b/example/basic-hooks/Basic/Store.purs new file mode 100644 index 0000000..14b5190 --- /dev/null +++ b/example/basic-hooks/Basic/Store.purs @@ -0,0 +1,17 @@ +module Hooks.Store where + +import Prelude + +type Store = { count :: Int } + +initialStore :: Store +initialStore = { count: 0 } + +data Action + = Increment + | Decrement + +reduce :: Store -> Action -> Store +reduce store = case _ of + Increment -> store { count = store.count + 1 } + Decrement -> store { count = store.count - 1 } diff --git a/example/basic-hooks/README.md b/example/basic-hooks/README.md new file mode 100644 index 0000000..c1fa0aa --- /dev/null +++ b/example/basic-hooks/README.md @@ -0,0 +1,3 @@ +# Hooks Example + +The basic-hooks example is yet another alternative to the basic example. It demonstrates how to access a small store from a single component (a counter) using hooks functionality (`useSeletor`) instead of stateful component. \ No newline at end of file diff --git a/example/basic-hooks/index.html b/example/basic-hooks/index.html new file mode 100644 index 0000000..844b31a --- /dev/null +++ b/example/basic-hooks/index.html @@ -0,0 +1,17 @@ + + + + Halogen Store - Basic with Hooks + + + + + + diff --git a/package.json b/package.json index 863c715..33cdfc4 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "examples": "npm run examples:basic && npm run examples:basic-no-action && npm run examples:redux-todo", "examples:basic": "spago -x example/example.dhall bundle-app --main Basic.Main --to example/basic/app.js", "examples:basic-no-action": "spago -x example/example.dhall bundle-app --main NoAction.Main --to example/basic-no-action/app.js", - "examples:redux-todo": "spago -x example/example.dhall bundle-app --main ReduxTodo.Main --to example/redux-todo/app.js" + "examples:redux-todo": "spago -x example/example.dhall bundle-app --main ReduxTodo.Main --to example/redux-todo/app.js", + "examples:basic-hooks": "spago -x example/example.dhall bundle-app --main Hooks.Main --to example/basic-hooks/app.js" } } diff --git a/spago.dhall b/spago.dhall index db95d52..d1918d6 100644 --- a/spago.dhall +++ b/spago.dhall @@ -4,11 +4,13 @@ , "effect" , "foldable-traversable" , "halogen" + , "halogen-hooks" , "halogen-subscriptions" , "maybe" , "prelude" , "refs" , "transformers" + , "tuples" , "unsafe-coerce" , "unsafe-reference" ] diff --git a/src/Halogen/Store/Hooks/UseSelector.purs b/src/Halogen/Store/Hooks/UseSelector.purs new file mode 100644 index 0000000..0f7a871 --- /dev/null +++ b/src/Halogen/Store/Hooks/UseSelector.purs @@ -0,0 +1,35 @@ +module Halogen.Store.Hooks.UseSelector where + +import Prelude + +import Data.Maybe (Maybe(..)) +import Data.Tuple.Nested ((/\)) +import Halogen.Hooks (class HookNewtype, type (<>), Hook, UseEffect, UseState) +import Halogen.Hooks as Hooks +import Halogen.Store.Monad (class MonadStore, emitSelected) +import Halogen.Store.Select (Selector(..)) + +foreign import data UseSelector :: Type -> Type -> Type -> Hooks.HookType + +type UseSelector' :: Type -> Type -> Type -> Hooks.HookType +type UseSelector' act store ctx = UseState (Maybe ctx) <> UseEffect <> Hooks.Pure + +instance newtypeUseSelector + :: HookNewtype (UseSelector act store ctx) (UseSelector' act store ctx) + +useSelector :: forall m act store ctx + . MonadStore act store m + => Selector store ctx + -> Hook m (UseSelector act store ctx) (Maybe ctx) +useSelector (Selector selector) = Hooks.wrap hook + where + hook :: Hook m (UseSelector' act store ctx) (Maybe ctx) + hook = Hooks.do + ctx /\ ctxId <- Hooks.useState Nothing + + Hooks.useLifecycleEffect do + emitter <- emitSelected (Selector selector) + subscriptionId <- Hooks.subscribe $ map (Hooks.put ctxId <<< Just) emitter + pure $ Just $ Hooks.unsubscribe subscriptionId + + Hooks.pure ctx \ No newline at end of file diff --git a/src/Halogen/Store/Monad.purs b/src/Halogen/Store/Monad.purs index 5af1f8f..904ea93 100644 --- a/src/Halogen/Store/Monad.purs +++ b/src/Halogen/Store/Monad.purs @@ -13,6 +13,7 @@ import Effect.Ref (Ref) import Effect.Ref as Ref import Halogen (HalogenM, hoist) import Halogen as H +import Halogen.Hooks as Hooks import Halogen.Store.Select (Selector(..)) import Halogen.Subscription (Emitter, Listener, makeEmitter) import Halogen.Subscription as HS @@ -98,6 +99,11 @@ instance monadStoreHalogenM :: MonadStore a s m => MonadStore a s (HalogenM st a updateStore = lift <<< updateStore emitSelected = lift <<< emitSelected +instance monadStoreHookM :: MonadStore a s m => MonadStore a s (Hooks.HookM m) where + getStore = lift getStore + updateStore = lift <<< updateStore + emitSelected = lift <<< emitSelected + -- | Run a component in the `StoreT` monad. -- | -- | Requires an initial value for the store, `s`, and a reducer that updates