@composi/runtime:

Update

Program Business Logic

A program's update method holds all its business logic. The update function listens for messages, when it receives one, it can check it to see which message it is and then run an appropriate action. By defining different actions, you instruct update how to handle them.

Update gets three arguments: state, message, and send. The first argument is the current state of the program. Message is any message sent by a user interaction with the view. And send allows you to send a message to another action. If the program is complex, it will probably require a number of messages being sent to update. Since send is a unary function, taking only one argument, it's convention to bundle everything up in an object literal with a type and data payload. Notice how we do this in the following snippet.

<p class='list-form'>
  <input autofocus onchange={getInputValue} type="text" />
  <button class='add-item' onclick={()=> send({type: 'add-item', data: inputValue})}>Add</button>
</p>

Knowing that the view will send an object with a type property for the message, we can set up the update function to test the message for various types. Here is a simple program. Notice how in its update method we check what message was sent, then perform approriate actions, such as "add-item", or "delete-item".


const program = {
  init() {
    return state
  },
  view(state, send) {
    return render(<List {...{ state, send }} />, section)
  },
  // Business Logic is in update method.
  // It intercepts actions sent by view.
  // Use those actions to transform state.
  // Then return the new state.
  // That will cause the view to update.
  update(state, msg, send) {
    // Clone state with destructing:
    const prevState = {...state}
    switch (msg.type) {
      case 'add-item':
        const value = msg.inputValue
        if (value) {
          prevState.fruits.push({ key: prevState.newKey++, value })
          return prevState
        } else {
          alert('Please provide a value!')
          return state
        }
        break
      case 'delete-item':
        prevState.fruits = prevState.fruits.filter(item => item.key != msg.key)
        return prevState
        break
    }
  }
}

And here is a working example:

See the Pen @composi/core + runtime basic by Robert Biggs (@rbiggs) on CodePen.

Note that no matter what decision we make with an action, we always return state. Every action must return state. Even if you make no changes to state, you have to return it. Failing to do so will generate an error. This is because actions pass state to the view to render, so not returning state will cause the view to fail because it never received state.

Just because you always return state, even when it doesn't change, doesn't mean that you cause unnecessary renders of the view. In fact, @composi/core's render function checks the state and component props early on and bails if they are identical.

Actions

Normally you would not define business logic inside the program's update method. This would make the program difficult to manage. Instead define the business logic in a separate funtion called actions. It will take the same three arguments as update:

// actions function to handle business logic
// for program's update method.
// We clone state in update method,
// which we pass to this function as
// its first argument.
function actions(state, msg, send) {
  switch (msg.type) {
    case 'add-item':
      const value = msg.inputValue
      if (value) {
        state.fruits.push({ key: state.newKey++, value })
        return state
      } else {
        alert('Please provide a value!')
        return state
      }
      break
    case 'delete-item':
      state.fruits = state.fruits.filter(item => item.key != msg.key)
      return state
      break
  }
}
const program = {
  init() {
    return state
  },
  view(state, send) {
    return render(<List {...{ state, send }} />, section)
  },
  // Update now returns the actions function
  // which contains the program's business logic:
  update(state, msg, send) {
    // Clone state with destructing:
    const prevState = {...state}
    // Pass prevState to actions:
    return actions(prevState, msg, send)
  }
}

Immutability

In the above example we used the JavaScript destructuring to create a clone of state before using it in the update method. In general this is good enough. However, if you would prefer to have a deep clone of state, you can use the clone function on state. clone comes pre-installed when you create a new @composi/core project. But you do need to import it into your project to use it:

import { h, render, run } from '@composi/core'
import { clone } from '@composi/merge-objects'
// With clone imported, you can use it to clone program state.
const program = {
  init() {
    return state
  },
  view(state, send) {
    return render(<List {...{ state, send }} />, section)
  },
  // Update now returns the actions function
  // which contains the program's business logic:
  update(state, msg, send) {
    // Create a deep clone of state:
    const prevState = clone(state)
    // Pass cloned state to actions:
    return actions(prevState, msg, send)
  }
}

About Redux

If you're familiar with Redux, you might notice some similarities with how update actions work. That's because Redux was inspired by the Elm architecture. The difference is in the detials. The messages sent by the view resemble Redux actions, and the update actions look like Redux reducers. They are similar, but different. Redux reducers are so called because they take the state, perform any number of operations and then return the new state. Runtime actions, on the other hand, can also just return the new state, or they can also fire off an effect. Because they can do more than one thing, they cannot be called reducers. Even so, in the vast majority of cases they will act just like Redux reducers. That's the most common use case.

Below is a working example of a program where the view sends several different messages to update, which examines them to see which type they are to preform the appropriate action. Examine it carefully to see how messages bundle up data to send to actions, which then update the state, causing the view to update. We've use a number of lifecycle event on the function component elements to handle things like input focus, input value, etc. We use an onchange event to get the value of the input so we can send it in a message to update.

See the Pen @composi/core + runtime by Robert Biggs (@rbiggs) on CodePen.

Alternative Approach

There is another way to send messages and capture them using a tagged union. @composi/core enables this through its union function. This basically creates an arrays of string values for function names and the actions to be used in update. This may sound complicated, but it's actually quite simple and results in cleaner and easier to read code. Go here to learn how to use tagged unions with update.