# Context

🚀 Quick Reference

While finite states are well-defined in finite state machines and statecharts, state that represents quantitative data (e.g., arbitrary strings, numbers, objects, etc.) that can be potentially infinite is represented as extended state (opens new window) instead. This makes statecharts much more useful for real-life applications.

In XState, extended state is known as context. Below is an example of how context is used to simulate filling a glass of water:

import { createMachine, assign } from 'xstate';

// Action to increment the context amount
const addWater = assign({
  amount: (context, event) => context.amount + 1
});

// Guard to check if the glass is full
function glassIsFull(context, event) {
  return context.amount >= 10;
}

const glassMachine = createMachine(
  {
    id: 'glass',
    // the initial context (extended state) of the statechart
    context: {
      amount: 0
    },
    initial: 'empty',
    states: {
      empty: {
        on: {
          FILL: {
            target: 'filling',
            actions: 'addWater'
          }
        }
      },
      filling: {
        // Transient transition
        always: {
          target: 'full',
          cond: 'glassIsFull'
        },
        on: {
          FILL: {
            target: 'filling',
            actions: 'addWater'
          }
        }
      },
      full: {}
    }
  },
  {
    actions: { addWater },
    guards: { glassIsFull }
  }
);

The current context is referenced on the State as state.context:

const nextState = glassMachine.transition(glassMachine.initialState, {
  type: 'FILL'
});

nextState.context;
// => { amount: 1 }

# Initial Context

The initial context is specified on the context property of the Machine:

const counterMachine = createMachine({
  id: 'counter',
  // initial context
  context: {
    count: 0,
    message: 'Currently empty',
    user: {
      name: 'David'
    },
    allowedToIncrement: true
    // ... etc.
  },
  states: {
    // ...
  }
});

The context property of the machine can also be initialized lazily; i.e., the context will not be created until the machine is actually created/used:

const counterMachine = createMachine({
  id: 'counter',
  // initial context
  context: () => ({
    count: 0,
    message: 'Currently empty',
    user: {
      name: 'David'
    },
    allowedToIncrement: true
    // ... etc.
  }),
  states: {
    // ...
  }
});

For dynamic context (that is, context whose initial value is retrieved or provided externally), you can use a machine factory function that creates the machine with the provided context values (implementation may vary):

const createCounterMachine = (count, time) => {
  return createMachine({
    id: 'counter',
    // values provided from function arguments
    context: {
      count,
      time
    }
    // ...
  });
};

const counterMachine = createCounterMachine(42, Date.now());

Or for existing machines, machine.withContext(...) should be used:

const counterMachine = createMachine({
  /* ... */
});

// retrieved dynamically
const someContext = { count: 42, time: Date.now() };

const dynamicCounterMachine = counterMachine.withContext(someContext);

The initial context of a machine can be retrieved from its initial state:

dynamicCounterMachine.initialState.context;
// => { count: 42, time: 1543687816981 }

This is preferred to accessing machine.context directly, since the initial state is computed with initial assign(...) actions and transient transitions, if any.

# Assign Action

The assign() action is used to update the machine's context. It takes the context "assigner", which represents how values in the current context should be assigned.

Argument Type Description
assigner object or function The object assigner or function assigner which assigns values to the context (see below)

The "assigner" can be an object (recommended):

import { createMachine, assign } from 'xstate';
// example: property assigner

// ...
  actions: assign({
    // increment the current count by the event value
    count: (context, event) => context.count + event.value,

    // assign static value to the message (no function needed)
    message: 'Count changed'
  }),
// ...

Or it can be a function that returns the updated state:

// example: context assigner

// ...

  // return a partial (or full) updated context
  actions: assign((context, event) => {
    return {
      count: context.count + event.value,
      message: 'Count changed'
    }
  }),
// ...

Both the property assigner and context assigner function signatures above are given 3 arguments: the context, event, and meta:

Argument Type Description
context TContext The current context (extended state) of the machine
event EventObject The event that triggered the assign action
meta 4.7+ AssignMeta an object with meta data (see below)

The meta object contains:

  • state - the current state in a normal transition (undefined for the initial state transition)
  • action - the assign action

WARNING

The assign(...) function is an action creator; it is a pure function that only returns an action object and does not imperatively make assignments to the context.

# Action Order

WARNING

In XState version 5, this behavior will change and assign(...) actions will be called in order instead of being prioritized, which is incorrect behavior according to SCXML.

To get this behavior in version 4, add preserveActionOrder: true to the machine config:

const counterMachine = createMachine({
  preserveActionOrder: true, // Ensures that assign actions are called in order
  // ...
  context: { count: 0 },
  states: {
    active: {
      on: {
        INC_TWICE: {
          actions: [
            (context) => console.log(`Before: ${context.count}`), // "Before: 0"
            assign({ count: (context) => context.count + 1 }), // count === 1
            assign({ count: (context) => context.count + 1 }), // count === 2
            (context) => console.log(`After: ${context.count}`) // "After: 2"
          ]
        }
      }
    }
  }
});

interpret(counterMachine).start().send({ type: 'INC_TWICE' });
// => "Before: 0"
// => "After: 2"

Custom actions are always executed with regard to the next state in the transition. When a state transition has assign(...) actions, those actions are always batched and computed first, to determine the next state. This is because a state is a combination of the finite state and the extended state (context).

For example, in this counter machine, the custom actions will not work as expected:

const counterMachine = createMachine({
  id: 'counter',
  context: { count: 0 },
  initial: 'active',
  states: {
    active: {
      on: {
        INC_TWICE: {
          actions: [
            (context) => console.log(`Before: ${context.count}`),
            assign({ count: (context) => context.count + 1 }), // count === 1
            assign({ count: (context) => context.count + 1 }), // count === 2
            (context) => console.log(`After: ${context.count}`)
          ]
        }
      }
    }
  }
});

interpret(counterMachine).start().send({ type: 'INC_TWICE' });
// => "Before: 2"
// => "After: 2"

This is because both assign(...) actions are batched in order and executed first (in the microstep), so the next state context is { count: 2 }, which is passed to both custom actions. Another way of thinking about this transition is reading it like:

When in the active state and the INC_TWICE event occurs, the next state is the active state with context.count updated, and then these custom actions are executed on that state.

A good way to refactor this to get the desired result is modeling the context with explicit previous values, if those are needed:

const counterMachine = createMachine({
  id: 'counter',
  context: { count: 0, prevCount: undefined },
  initial: 'active',
  states: {
    active: {
      on: {
        INC_TWICE: {
          actions: [
            (context) => console.log(`Before: ${context.prevCount}`),
            assign({
              count: (context) => context.count + 1,
              prevCount: (context) => context.count
            }), // count === 1, prevCount === 0
            assign({ count: (context) => context.count + 1 }), // count === 2
            (context) => console.log(`After: ${context.count}`)
          ]
        }
      }
    }
  }
});

interpret(counterMachine).start().send({ type: 'INC_TWICE' });
// => "Before: 0"
// => "After: 2"

The benefits of this are:

  1. The extended state (context) is modeled more explicitly
  2. There are no implicit intermediate states, preventing hard-to-catch bugs
  3. The action order is more independent (the "Before" log can even go after the "After" log!)
  4. Facilitates testing and examining the state

# Notes

  • 🚫 Never mutate the machine's context externally. Everything happens for a reason, and every context change should happen explicitly due to an event.
  • Prefer the object syntax of assign({ ... }). This makes it possible for future analysis tools to predict how certain properties can change declaratively.
  • Assignments can be stacked, and will run sequentially:
// ...
  actions: [
    assign({ count: 3 }), // context.count === 3
    assign({ count: context => context.count * 2 }) // context.count === 6
  ],
// ...
  • Just like with actions, it's best to represent assign() actions as strings or functions, and then reference them in the machine options:




 










const countMachine = createMachine({
  initial: 'start',
  context: { count: 0 }
  states: {
    start: {
      entry: 'increment'
    }
  }
}, {
  actions: {
    increment: assign({ count: context => context.count + 1 }),
    decrement: assign({ count: context => context.count - 1 })
  }
});

Or as named functions (same result as above):









 





const increment = assign({ count: context => context.count + 1 });
const decrement = assign({ count: context => context.count - 1 });

const countMachine = createMachine({
  initial: 'start',
  context: { count: 0 }
  states: {
    start: {
      // Named function
      entry: increment
    }
  }
});
  • Ideally, the context should be representable as a plain JavaScript object; i.e., it should be serializable as JSON.
  • Since assign() actions are raised, the context is updated before other actions are executed. This means that other actions within the same step will get the updated context rather than what it was before the assign() action was executed. You shouldn't rely on action order for your states, but keep this in mind. See action order for more details.

# TypeScript

For proper type inference, add the context type to the schema property of the machine:

import { createMachine } from 'xstate';

interface CounterContext {
  count: number;
  user?: {
    name: string;
  };
}

const machine = createMachine({
  schema: {
    context: {} as CounterContext
  },
  // ...
  context: {
    count: 0,
    user: undefined
  }
  // ...
});

When applicable, you can also use typeof ... as a shorthand:

const context = {
  count: 0,
  user: { name: '' }
};

const machine = createMachine({
  schema: {
    context: {} as typeof context
  },
  // ...
  context
  // ...
});

In most cases, the types for context and event in assign(...) actions will be automatically inferred from the type parameters passed into schema:

interface CounterContext {
  count: number;
}

const machine = createMachine({
  schema: {
    context: {} as CounterContext
  },
  // ...
  context: {
    count: 0
  },
  // ...
  {
    on: {
      INCREMENT: {
        // Inferred automatically in most cases
        actions: assign({
          count: (context) => {
            // context: { count: number }
            return context.count + 1;
          }
        })
      }
    }
  }
});

However, TypeScript inference isn't perfect, so the responsible thing to do is to add the context and event as generics into assign<Context, Event>(...):



 











// ...
on: {
  INCREMENT: {
    // Generics guarantee proper inference
    actions: assign<CounterContext, CounterEvent>({
      count: (context) => {
        // context: { count: number }
        return context.count + 1;
      }
    });
  }
}
// ...

# Quick Reference

Set initial context

const machine = createMachine({
  // ...
  context: {
    count: 0,
    user: undefined
    // ...
  }
});

Set dynamic initial context

const createSomeMachine = (count, user) => {
  return createMachine({
    // ...
    // Provided from arguments; your implementation may vary
    context: {
      count,
      user
      // ...
    }
  });
};

Set custom initial context

const machine = createMachine({
  // ...
  // Provided from arguments; your implementation may vary
  context: {
    count: 0,
    user: undefined
    // ...
  }
});

const myMachine = machine.withContext({
  count: 10,
  user: {
    name: 'David'
  }
});

Assign to context

const machine = createMachine({
  // ...
  context: {
    count: 0,
    user: undefined
    // ...
  },
  // ...
  on: {
    INCREMENT: {
      actions: assign({
        count: (context, event) => context.count + 1
      })
    }
  }
});

Assignment (static)

// ...
actions: assign({
  counter: 42
}),
// ...

Assignment (property)

// ...
actions: assign({
  counter: (context, event) => {
    return context.count + event.value;
  }
}),
// ...

Assignment (context)

// ...
actions: assign((context, event) => {
  return {
    counter: context.count + event.value,
    time: event.time,
    // ...
  }
}),
// ...

Assignment (multiple)

// ...
// assume context.count === 1
actions: [
  // assigns context.count to 1 + 1 = 2
  assign({ count: (context) => context.count + 1 }),
  // assigns context.count to 2 * 3 = 6
  assign({ count: (context) => context.count * 3 })
],
// ...