Skip to content

component states

jiyinyiyong edited this page Apr 4, 2020 · 8 revisions

Component States

Unlike React, states in Respo is maintained manually for stablility during hot code swapping. At first, states is a HashMap inside the store:

(defonce *store {:states {}})

By design, if states is added, you would a tree:

{
  :states {
    :data {}
    :todolist {
      :data {:input "xyz..."}
      "task-1-id" {:data {:draft "xxx..."}}
      "task-2-id" {:data {:draft "yyy..."}}
      "task-2-id" {:data {:draft "zzz.."}}
    }
  }
}

:data is a special field for holding state of each component. It has to be a tree since Virtual DOM is a tree. You also notice that its structure is simpler than a DOM tree, it only contains states.

respo.core/>> is the "picking branch" function. It also maintains a :cursor field.

When you call (>> states :todolist), you get new states variable for a child component:

{
  :cursor [:todolist] ; generated cursor, nil at top level
  :data {:input "xyz..."} ; state at current level

  "task-1-id" {:data {:draft "xxx..."}} ; states for children
  "task-2-id" {:data {:draft "yyy..."}}
  "task-2-id" {:data {:draft "zzz.."}}
}

Then you call (>> states "task-1-id") and you get new states for child "task-1":

{:cursor [:todolist "task-1-id"] ; generated cursor
 :data {:draft "xxx..."} ; state of task-1

For state inside each component, it's nil at first. You want to have an initial state, use or to provide one.

(defcomp comp-task [states]
  (let [cursor (:cursor states)
        state (or (:data states) {:draft "empty"})]
    (div {})))

By accessing (:data states), you get nil, so {:draft "empty"} is used. After there's data in states, you get data that was set.

Then you want to update component s:tate

(defcomp comp-task [states]
  (let [cursor (:cursor states)
        state (or (:data states) {:draft "empty"})]
    (div {:on-click (fn [e dispatch!]
                        (dispatch! cursor (assoc state :draft "New state")))})))

So (dispatch! cursor state) sends the new state.

The last step to to update global states with respo.cursor/update-states. Internally (dispatch! cursor op-data) will be transformed to (dispatch! :states [cursor op-data]). And then in updater you add:

(case op
  :states (update-states store op-data)) ; where op-data is [cursor new-state]
  ; other actions
  (do store)

Let's wrap it. First we have empty states inside store:

{:states {}}

And it is passed to (comp-todolist (>> states :todolist) data), and then passed to (comp-task (>> states (:id task)) task).

In comp-todolist, (:data states) provides component state, (:cursor states) provides its cursor. Call (dispatch! cursor {:input "New draft"}) and global store will become:

{
  :states {
    :todolist {
      :data {:input "New draft"}
    }
  }
}

In comp-task of "task-1", you also get state and cursor, so call (dispatch! cursor {:draft "New text"}) you will get:

{
  :states {
    :todolist {
      :data {:input "New draft"}
      "task-1-id" {:data {:draft "New text"}}
    }
  }
}

And that's how Respo states is maintained.

Clone this wiki locally