Marko

State

The output of a component is based on input properties passed from its parent as attributes. However, a component may also maintain internal state that it uses to control its view. If Marko detects a change to either input or to the internal state, the view will automatically be updated.

ProTip: Only data that is owned and modified by the component should go into its state. State should be exclusively used for data that triggers rerenders. Parents control input, and the component controls its own state.

Initializing state

To use state in Marko, you must first create a class component and initialize the state within the onCreate method. In class methods, this.state may be used and within the template section, a state variable is available.

class {
    onCreate() {
        this.state = { count: 0 };
    }
}

<div>The count is ${state.count}</div>
class {
  onCreate() {
    this.state = { count: 0 };
  }
}

div -- The count is ${state.count}

Note: Only properties that exist when this.state is first defined will be watched for changes. If you don't need a property initially, you can set it to null.

Updating state

You can update state in response to DOM events, browser events, ajax calls, etc. When a property on the state changes, the view will be updated to match.

class {
    onCreate() {
        this.state = { count: 0 };
    }
    increment() {
        this.state.count++;
    }
}

<div>The count is ${state.count}</div>
<button on-click('increment')>Increment</button>
class {
  onCreate() {
    this.state = { count: 0 };
  }
  increment() {
    this.state.count++;
  }
}

div -- The count is ${state.count}
button on-click("increment") -- Increment

We've extended our example above to add a button with an event handler, so that, when clicked, the state.count value is incremented.

Note: When browsing existing code, you may see this.setState('name', value) being used. This is equivalent to this.state.name = value.

How updates work

When a property on state is set, the component will be scheduled for an update if the property has changed. All updates are batched together for performance. This means you can update multiple state properties at the same time without causing multiple updates.

ProTip: If you need to know when the update has been applied, you can use this.once('update', fn) within a component method.

Note: The state object only watches its properties one level deep. This means updates to nested properties on the state (e.g. this.state.object.something = newValue) will not be detected.

Using immutable data structures is recommended, but if you want to mutate a state property (perhaps push a new item into an array) you can let Marko know it changed using setStateDirty.

this.state.numbers.push(num);

// mark numbers as dirty, because a `push`
// won't be automatically detected by Marko
this.setStateDirty("numbers");

Cross component state management

There are various tools available to manage state outside of a single component. Here are some basic guidelines.

Typically we recommend using attributes to pass data in to a child component, and children can emit events to communicate back up to their parents. In some cases this can become cumbersome with deeply nested data dependencies or global state.

Global/Subtree

For passing state throughout a component tree without explicit attribute setting throughout the entire app, you can leverage the <context> tag. This tag can be installed from npm.

This tag allows you to pull state from any level above in the tree and can also be used to pass global state throughout your app. Context providers can register event handlers that any child in the tree can trigger similar to the events API.

fancy-form.marko
<context coupon=input.coupon on-buy(handleBuy)>
    <!-- Somewhere nested in the container will be the buy button -->
    <fancy-container/>
</context>
context coupon=input.coupon on-buy(handleBuy)
  <!-- Somewhere nested in the container will be the buy button -->
  fancy-container
fancy-save-button.marko
<context|{ coupon }, emit| from="fancy-form">
    Coupon: ${coupon}.
    <button on-click(emit, "buy")>Buy</button>
</context>
context|{ coupon }, emit| from="fancy-form"
  -- Coupon: ${coupon}.
  button on-click(emit, "buy") -- Buy

Note: Context couples tags together and can limit reuse of components.

When to use a Redux like pattern

Often the above two approaches are enough, and many people jump to this part far too quickly. Like <context>, often anything stored in redux is global. This means that it can (if abused) create components that are hard to reuse, reason about and test. However it is important to understand when a tool like redux is useful in any UI library.

Redux provides indirection to updating any state that it controls. This is useful if you need the following:

  • Single state update, multiple actions (eg: logging, computed data, etc).
  • Time travel debugging and other redux-specific tooling.
EDIT on GitHub

Contributors

Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.

Chat in Marko's Discord Server