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 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.
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.