Server-Side Components

Introduction

IHP Server-Side Components provide a toolkit for building interactive client-side functionality without needing to write too much JavaScript.

A Server-Side Component consist of a state object, a set of actions and a render function.

The typical lifecycle is like this:

  1. The component is rendered based as part of a view
  2. Once loaded, elements inside the component, e.g. a button, can call server-side actions using a simple javascript library
  3. On the server-side the actions are evaluated and a new state object is generated
  4. The new state will trigger a re-render
  5. The re-rendered content will be diffed with the existing HTML and then HTML update instructions will be sent to client and the view is updated accordingly
  6. Repeat at step 2
  7. The component is stopped when the page is closed

The Server-Side Component toolkit is currently still in a early development stage. So expect bugs and API changes.

Creating a Component

In this example we’re building a counter component: The counter shows a number. When a button is clicked the number will be incremented.

To create this new component first we’re creating a new file at Web/Component/Counter.hs (the Component directory likely does not exist yet, so you need to create it):

module Web.Component.Counter where

import IHP.ViewPrelude
import IHP.ServerSideComponent.Types
import IHP.ServerSideComponent.ControllerFunctions

-- The state object
data Counter = Counter { value :: !Int }

-- The set of actions
data CounterController
    = IncrementCounterAction
    | SetCounterValue { newValue :: !Int }
    deriving (Eq, Show, Data)

$(deriveSSC ''CounterController)

instance Component Counter CounterController where
    initialState = Counter { value = 0 }

    -- The render function
    render Counter { value } = [hsx|
        Current: {value} <br />
        <button onclick="callServerAction('IncrementCounterAction')">Plus One</button>
        <hr />
        <input type="number" value={inputValue value} onchange="callServerAction('SetCounterValue', { newValue: parseInt(this.value, 10) })"/>
    |]
    
    -- The action handlers
    action state IncrementCounterAction = do
        state
            |> incrementField #value
            |> pure

    action state SetCounterValue { newValue } = do
        state
            |> set #value newValue
            |> pure

instance SetField "value" Counter Int where setField value' counter = counter { value = value' }

You can see that the Counter component has a state object with a number data Counter = Counter { value :: !Int }. It has two actions IncrementCounterAction and SetCounterValue. The initialState = Counter { value = 0 } means that the counter starts at 0.

Inside the render function you can see how server-side actions are triggered from the client-side:

<button onclick="callServerAction('IncrementCounterAction')">Plus One</button>

When the callServerAction('IncrementCounterAction') is called, it will trigger the action state IncrementCounterAction = do haskell block to be called on the server.

You can see that the action handler get’s passed the current state and will return a new state based on the action and the current state.

FrontController

To make the component available to the app, we need to add it to our Web.FrontController.

Open the Web/FrontController.hs and add these imports:

import IHP.ServerSideComponent.RouterFunctions
import Web.Component.Counter

Inside the instance FrontController WebApplication add a routeComponent @Counter:


instance FrontController WebApplication where
    controllers = 
        [ startPage WelcomeAction
        -- ...
        , routeComponent @Counter
        ]

Now the websocket server for Counter is activated.

Using the Component

We’re adding the component to the standard Welcome view inside our project. But you can use it basically on any view you want.

Open the Web/View/Static/Welcome.hs and add these imports:

import IHP.ServerSideComponent.ViewFunctions
import Web.Component.Counter

Now we change the welcome view to this:

instance View WelcomeView where
    html WelcomeView = [hsx|
        <h1>Counter</h1>

        {counter}
    |]
        where
            counter = component @Counter

If you wonder why we’re using the where instead of writing {component @Counter}: Currently the at-symbol @ is not supported in HSX expressions.

We also need to load the ihp-ssc.js from our Layout.hs. Open the Web/View/Layout.hs and add <script src="/vendor/ihp-ssc.js"></script> inside your scripts section:

scripts :: Html
scripts = [hsx|
        <!-- ... ->
        <script src="/vendor/ihp-ssc.js"></script>
    |]

Now when opening the WelcomeView you will see the newly created counter.

Advanced

Actions with Parameters

Let’s say we have actions like this:

data BooksTableController
    = SetSearchQuery { searchQuery :: Text }
    | SetOrderBy { column :: Text }
    deriving (Eq, Show, Data, Read)

To call the SetSearchQuery action with a specific searchQuery value, we can pass this to callServerAction:

<input
    type="text"
    value={inputValue searchQuery}
    onkeyup="callServerAction('SetSearchQuery', { searchQuery: this.value })"
/>

Fetching from the Database

You can use the typical IHP database operations like query @Post or createRecord from your actions.

To fill the inital data you can use the componentDidMount lifecycle function:

data PostsTable = PostsTable
    { posts :: Maybe [Post]
    }
    deriving (Eq, Show)


instance Component PostsTable PostsTableController where
    initialState = PostsTable { posts = Nothing }

    componentDidMount state = do
        books <- query @Post |> fetch

        state
            |> setJust #posts posts
            |> pure

    render PostsTable { .. } = [hsx|
        {when (isNothing posts) loadingIndicator}
        {forEach posts renderPost}
    |]

The componentDidMount get’s passed the initial state and returns a new state. It’s called right after the first render once the client has wired up the WebSocket connection.

When the posts field is set to Nothing we know that the data is still being fetched. In that case we render a loading spinner inside our render function.

HTML Diffing & Patching

IHP uses a HTML Diff & Patch approach to update the components HTML. You can see this by analysing the data that is sent over the WebSocket connection.

In the above example, when the Plus One button of the counter is clicked, the client will send the following message to the server using the WebSocket connection:

{"action":"IncrementCounterAction"}

After that the server will respond:

[{"type":"UpdateTextContent","textContent":"Current: 1","path":[0]}]

So the server only responds with update instructions that transform the counter’s Current: 0 to Counter: 1.

This is useful if you have many interactive elements that are controlled by JavaScript libraries (e.g. a. <video> element that is playing). As long as the HTML code of these interactive elements doesn’t change on the server-side, the DOM nodes will not be touched by IHP.

Example Components

If you want to see some more code, you can find components inside the IHP SSC Playground or inside the ihp-ssc-block-editor-demo repository.