By default, HyperViews are stateless. Nothing is stored in the server connection. HyperViewupdates are the direct result of processing the Action.
instance HyperView Swapper es where
data Action Swapper = Hello | Goodbye
deriving (Generic, ViewAction)
update Hello = pure "Hello"
update Goodbye = pure "Goodbye"
The simplest way to add state to a HyperView is to pass it back and forth between the Action and the View. In this implementation of the classic counter example, each constructor of the Action expects an Int, which represents the current count:
instance HyperView Counter es where
data Action Counter
= Increment Int
| Decrement Int
deriving (Generic, ViewAction)
update (Increment n) = do
pure $ viewCount (n + 1)
update (Decrement n) = do
pure $ viewCount (n - 1)
Our View Function also expects the current count as an input. It includes it in each Action, and the cycle continues:
viewCount :: Int -> View Counter ()
viewCount n = row $ do
col ~ gap 10 $ do
el ~ dataFeature $ text $ pack $ show n
row ~ gap 10 $ do
button (Decrement n) "Decrement" ~ Style.btn
button (Increment n) "Increment" ~ Style.btn
page :: (Hyperbole :> es) => Page es '[Counter]
page = do
pure $ do
hyperState CounterState 1 viewCount
State (ViewState viewId) :> es is already included in update, and can be accessed with familiar functions like getput and modify from Effectful.State.Dynamic
instance HyperView Counter es where
data Action Counter
= Increment
| Decrement
deriving (Generic, ViewAction)
update Increment = do
modify @Int (+ 1)
pure viewCount
update Decrement = do
modify @Int (subtract 1)
pure viewCount
To read the state in View use the viewState function
viewCount :: View Counter ()
viewCount = row $ do
n <- viewState
col ~ gap 10 $ do
el ~ dataFeature $ text $ pack $ show n
row ~ gap 10 $ do
button Decrement "Decrement" ~ btn
button Increment "Increment" ~ btn
The state Action threading and ViewState both live in on the web page itself, and are reset when the user navigates away or refreshes the browser.
Using the Hyperbole effect, we can store state in the browser Query string. This is useful for faceted search, or any time a user might want to share a url and have the page load with local state changes.
data Preferences = Preferences
{ message :: Text
, color :: AppColor
}
deriving (Generic, Show, ToQuery, FromQuery)
instance Default Preferences where
def = Preferences mempty def
Likewise we can store state in a browser cookie using Session. This is useful for user preferences, login state, and any time you want client-specific state to persist across navigation and refreshes
data Preferences = Preferences
{ message :: Text
, color :: AppColor
}
deriving (Generic, Show, ToEncoded, FromEncoded, Session)
instance Default Preferences where
def = Preferences "_" White
State stored in the Session is page-specific by default, but can be configured to be application-wide. It survives until manually cleared by the user or the application.
For any real application, most persistent state will need to use a separate Effect, like a database. In Side Effects we demonstrated how to use a custom effect to wrap a database.
Another way to store application-wide state is to use the Concurrent effect. It gives us the ability to work with TVars, MVars and STM. Here is another counter, implemented using Reader (TVar Int)
page :: (Hyperbole :> es, Concurrent :> es, Reader (TVar Int) :> es) => Page es '[Counter]
page = do
n <- getCount
pure $ do
hyper Counter (viewCount n)
getCount :: (Concurrent :> es, Reader (TVar Int) :> es) => Eff es Int
getCount = readTVarIO =<< ask
Notice how this state is shared among all users application-wide, and survives until the app restarts