diff --git a/src/React/Basic/Hooks/Aff.purs b/src/React/Basic/Hooks/Aff.purs index 14d7973..340e0a0 100644 --- a/src/React/Basic/Hooks/Aff.purs +++ b/src/React/Basic/Hooks/Aff.purs @@ -1,15 +1,21 @@ module React.Basic.Hooks.Aff ( useAff , UseAff + , useAffReducer + , AffReducer + , noEffects + , UseAffReducer ) where import Prelude import Data.Either (Either(..)) +import Data.Foldable (for_) import Data.Maybe (Maybe(..)) import Data.Newtype (class Newtype) +import Effect (Effect) import Effect.Aff (Aff, Error, error, killFiber, launchAff, launchAff_, throwError, try) import Effect.Class (liftEffect) -import React.Basic.Hooks (Hook, UseEffect, UseState, coerceHook, unsafeRenderEffect, useEffect, useState, (/\)) +import React.Basic.Hooks (type (/\), Hook, UnsafeReference(..), UseEffect, UseReducer, UseState, coerceHook, unsafeRenderEffect, useEffect, useReducer, useState, (/\)) import React.Basic.Hooks as React -- | `useAff` is used for asynchronous effects or `Aff`. The asynchronous effect @@ -48,3 +54,61 @@ newtype UseAff deps a hooks = UseAff (UseEffect deps (UseState (Maybe (Either Error a)) hooks)) derive instance ntUseAff :: Newtype (UseAff deps a hooks) _ + +-- | Provide an initial state and a reducer function. This is a more powerful +-- | version of `useReducer`, where a state change can additionally queue +-- | asynchronous operations. The results of those operations must be mapped +-- | into the reducer's `action` type. This is essentially the Elm architecture. +-- | +-- | Generally, I recommend `useAff` paired with tools like `useResetToken` over +-- | `useAffReducer` as there are many ways `useAffReducer` can result in race +-- | conditions. `useAff` with proper dependency management will handle previous +-- | request cancellation and ensure your `Aff` result is always in sync with +-- | the provided `deps`, for example. To accomplish the same thing with +-- | `useAffReducer` would require tracking `Fiber`s manually in your state +-- | somehow.. :c +-- | +-- | That said, `useAffReducer` can still be helpful when converting from the +-- | current `React.Basic` (non-hooks) API or for those used to Elm. +-- | +-- | *Note: Aff failures are thrown. If you need to capture an error state, be +-- | sure to capture it in your action type!* +useAffReducer :: + forall state action. + state -> + AffReducer state action -> + Hook (UseAffReducer state action) (state /\ (action -> Effect Unit)) +useAffReducer initialState reducer = + coerceHook React.do + { state, effects } /\ dispatch <- + useReducer { state: initialState, effects: [] } (_.state >>> reducer) + useEffect (UnsafeReference effects) do + for_ effects \aff -> + launchAff_ do + actions <- aff + liftEffect do for_ actions dispatch + mempty + pure (state /\ dispatch) + +type AffReducer state action + = state -> + action -> + { state :: state + , effects :: Array (Aff (Array action)) + } + +noEffects :: + forall state action. + state -> + { state :: state + , effects :: Array (Aff (Array action)) + } +noEffects state = { state, effects: [] } + +newtype UseAffReducer state action hooks + = UseAffReducer + ( UseEffect (UnsafeReference (Array (Aff (Array action)))) + (UseReducer { state :: state, effects :: Array (Aff (Array action)) } action hooks) + ) + +derive instance ntUseAffReducer :: Newtype (UseAffReducer state action hooks) _