Counter

Our first example is a simple counter that can be incremented or decremented. I find that it can be helpful to see the entire program in one place, so here it is! We will break it down afterwards.

import {html, forward, start} from "reflex"
import type {DOM, Address} from "reflex"

// Model
export class Model {
  value: number;
  constructor(value:number) {
    this.value = value
  }
}

export const init = (value:number=0) =>
  new Model(value)


// Update

export type Message =
  | { type: "Increment" }
  | { type: "Decrement" }

export const update = (model:Model, message:Message):Model => {
    switch (message.type) {
      case "Increment":
        return increment(model)
      case "Decrement":
        return decrement(model)
    }
  }

const increment = ({value}) => new Model(value + 1)
const decrement = ({value}) => new Model(value - 1)

export const view = (model:Model, address:Address<Message>):DOM =>
  html.section({
    className: "counter"
  }, [
    html.button({
      onClick: forward(address, createIncrement)
    }, ["-"]),
    html.output({ 
      className: "value"
    }, [`${model.value}`]),
    html.button({
      onClick: forward(address, createDecrement)
    }, ["+"])
  ])

const createIncrement = () => { type: "Increment" }
const createDecrement = () => { type: "Decrement" }

export main = start(beginner({ model, update, view }))

That's everything!

Note: This section has import type / export type and other type annotations like value:number. Those are flow syntax extensions. You do not need to deeply understand that stuff now, but you are free to jump ahead if it helps.

Model

When writing this program from scratch, it's best to start by taking a guess at the model. To make a counter, we at least need to keep track of it's value that is going up and down.

// Model
export class Model {
  value: number;
  constructor(value:number) {
    this.value = value
  }
}

export const init = (value:number=0) =>
  new Model(value)

Note: It is recommended to treat Model as a logicless stuct and provide init function to construct it and put all the logic in it.

Update

Now that we have a model, we need to define how it changes over time. It is best to start Update section by defining a set of messages that component may receive.

export type Message =
  | { type: "Increment" }
  | { type: "Decrement" }

Given that counter can increment or decrement the value Message type describes those options. Important! From there, the update function just describes what to do on one of these messages.

export const update = (model:Model, message:Message):Model => {
    switch (message.type) {
      case "Increment":
        return increment(model)
      case "Decrement":
        return decrement(model)
    }
  }

Note: At the moment of writing flow's pattern matching is not exhaustive (see #451) which in plain English means default case is always required in switch statements. So Above example should actually be written as:

  export const update = (model:Model, message:Message):Model => {
      switch (message.type) {
        case "Increment":
          return increment(model)
        case "Decrement":
          return decrement(model)
        default:
          throw TypeError('Unsupported message was received')
      }
    }

This significantly reduces set of guarantees that type checker can provide, but we hope that flow will close this gap soon enough.

If Increment message is received then increment the model. If Decrement message is received then decrement the model. Pretty straight-forward stuff.

const increment = ({value}) => new Model(value + 1)
const decrement = ({value}) => new Model(value - 1)

Note: It is considered idiomatic to have separate function for each update path and dispatch to one of them from update.

View

In Reflex Virtual DOM trees are constructed through functions in the html namespace. If you are able to read HTML markup you should be able to read this as well.

Note: No JSX support is provided out of the box as we don't really buy into it's value proposition, but it should not be too hard to support it.

export const view = (model:Model, address:Address<Message>):DOM =>
  html.section({
    className: "counter"
  }, [
    html.button({
      onClick: forward(address, createIncrement)
    }, ["-"]),
    html.output({ 
      className: "value"
    }, [`${model.value}`]),
    html.button({
      onClick: forward(address, createDecrement)
    }, ["+"])
  ])

const createIncrement = _ => ({ type: "Increment" })
const createDecrement = _ => ({ type: "Decrement" })

Notice that address: Address<Message> argument passed to the view function. That is mechanism for sending messages of Message type back into the update loop.

Note: Type checker will ensure that messages passed to address are of Message type as that is what earlier defined update expects.

Another thing to notice is that forward function, which is highly optimized utility function for composing functions:

const forward = <a, b>
  (address:Address<a>, tag:(input:b) => a):Address<b>
  (message) => address(tag(message))

results matching ""

    No results matching ""