State Management

State is managed using stateful components and hooks. We previously saw how to create a simple state variable using the state hook. Here we will take a look at using the reducer hook to model more complex state and transformations. We'll also utilize the handler hook to help us dispatch events to the reducer.

Reducer Functions

Reducer functions are functions that take a state and message and return a new state. They are used to update state in response to events. Let's create a hello_button component as an example, we can define a function that updates the state when the button is clicked.

First we define our state struct and message types:

type Model {
  Model(selection: Option(Int), options: List(HelloOption))
}

type Msg {
  NoOp
  SayHello
}

Here we're storing a list of options and the index of the selected option in the state.

Next we define our update function:

fn update(model: Model, msg: Msg) -> Model {
  case msg {
    NoOp -> model
    SayHello ->
      Model(..model, selection: Some(int.random(0, list.length(model.options))))
  }
}

An update function takes the current state and a message and returns a new state.

Introducing the Reducer Hook

Let's declare a reducer hook in our component that initializes the state model and uses our update function:

use ctx, Model(selection: selection, options: options), dispatch <- reducer(
  ctx,
  Model(selection: None, options: hello_options()),
  update,
)

You can see here we are provided with the current state of the reducer, which we can use in our component. Notice, we also are provided with a dispatch function from the reducer. The dispatch function is used to send messages to the reducer which will update the state and trigger a re-render.

The second argument, Model(selection: None, options: hello_options()), defines an initial state for the reducer using the predefined set options from hello_options which is used to initialize the state of the reducer when the component is mounted.

We need one more thing to complete our component. We need to define a function that will be called when the button is clicked. It's important that we create an IdentifiableHandler by using the handler hook so that we can ensure the id of the handler function is consistent across renders, preventing a new id being created and sent to the client on every render.

use ctx, say_hello <- handler(
  ctx,
  fn(_) { dispatch(SayHello) },
)

Putting it all together

We now have all the pieces we need to create a more interesting button that updates whenever it is clicked. Let's put it all together:

import gleam/int
import gleam/list
import gleam/option.{None, Option, Some}
import sprocket/context.{Context}
import sprocket/component.{render}
import sprocket/hooks.{State, reducer, handler}
import sprocket/html/elements.{button, div, span, text}
import sprocket/html/attributes.{class, on_click}

type Model {
  Model(selection: Option(Int), options: List(HelloOption))
}

type Msg {
  NoOp
  SayHello
}

fn update(model: Model, msg: Msg) -> Model {
  case msg {
    NoOp -> model
    SayHello ->
      Model(..model, selection: Some(int.random(list.length(model.options))))
  }
}

pub type HelloButtonProps {
  HelloButtonProps
}

pub fn hello_button(ctx: Context, _props: HelloButtonProps) {
  use ctx, Model(selection: selection, options: options), dispatch <- reducer(
    ctx,
    Model(selection: None, options: hello_options()),
    update,
  )

  use ctx, say_hello <- handler(
    ctx,
    fn(_) { dispatch(SayHello) },
  )

  // find the selected option using the selection index and list of options
  let hello =
    selection
    |> option.map(fn(i) {
      list.at(options, i)
      |> option.from_result()
    })
    |> option.flatten()

  render(
    ctx,
    div(
      [],
      [
        button(
          [
            class("p-2 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white rounded"),
            on_click(say_hello),
          ],
          [text("Say Hello!")],
        ),
        ..case hello {
          None -> []
          Some(hello) -> [
            span([class("ml-2")], [text(hello.1)]),
            span(
              [class("ml-2 text-gray-400 bold")],
              [text(hello.0)],
            ),
          ]
        }
      ],
    ),
  )
}

type HelloOption =
  #(String, String)

fn hello_options() -> List(HelloOption) {
  [
    #("English", "Hello"),
    #("Spanish", "Hola"),
    #("French", "Bonjour"),
    #("German", "Hallo"),
    #("Italian", "Ciao"),
    #("Portuguese", "Olá"),
    #("Hawaiian", "Aloha"),
    #("Chinese (Mandarin)", "你好,(Nǐ hǎo)"),
    #("Japanese", "こんにち, (Konnichiwa)"),
    #("Korean", "안녕하세, (Annyeonghaseyo)"),
    #("Arabic", "مرحب, (Marhaba)"),
    #("Hindi", "नमस्त, (Namaste)"),
    #("Turkish", "Merhaba"),
    #("Dutch", "Hallo"),
    #("Swedish", "Hej"),
    #("Norwegian", "Hei"),
    #("Danish", "Hej"),
    #("Greek", "Γεια σας,(Yia sas)"),
    #("Polish", "Cześć"),
    #("Swahili", "Hujambo"),
  ]
}

We now have a functional button that says hello in a different language when it's clicked.

Remember, all of these state changes are happening on the server. Events are being passed from the client to the server, the latest view is rendered and a minimal diff update is sent back to the client a which is then patched into the DOM!

These are just two of the hooks that are available in Sprocket. There are many more to explore! We'll cover hooks more in-depth in the next section.