Skip to main content
Interface X uses Vuex for centralized state management, with each X Module having its own namespaced store module. The entire system is fully type-safe, providing autocomplete and compile-time error checking throughout.

Store Architecture

All module stores are registered under a root x namespace for clean scoping:
// From: store/store.types.ts
export interface RootXStoreState {
  x: {
    [Module in XModuleName]: ExtractState<Module>
  }
}

// Actual state structure:
{
  x: {
    search: { query: '', results: [], ... },
    facets: { facets: [], selectedFilters: [], ... },
    searchBox: { query: '', ... },
    // ... all modules
  }
}
The x namespace prevents conflicts with your own Vuex modules and makes it clear which state belongs to Interface X.

Type-Safe Store Modules

Each module’s store is strongly typed using the XStoreModule interface:
// From: store/store.types.ts
export interface XStoreModule<
  State extends Record<keyof State, any>,
  Getters extends Record<keyof Getters, any>,
  Mutations extends MutationsDictionary<Mutations>,
  Actions extends ActionsDictionary<Actions>,
> {
  actions: ActionsTree<State, Getters, Mutations, Actions>
  getters: GettersTree<State, Getters>
  mutations: MutationsTree<State, Mutations>
  state: () => State
}

Example: Search Module Store

Let’s examine the real search module store from the source:
// From: x-modules/search/store/module.ts
export const searchXStoreModule: SearchXStoreModule = {
  state: () => ({
    // Resettable state
    query: '',
    results: [],
    partialResults: [],
    facets: [],
    relatedTags: [],
    banners: [],
    promoteds: [],
    totalResults: 0,
    spellcheckedQuery: '',
    sort: '',
    page: 1,
    origin: null,
    isAppendResults: false,
    redirections: [],
    queryTagging: { url: '', params: {} },
    displayTagging: { url: '', params: {} },
    stats: {} as Stats,
    
    // Persistent state
    selectedFilters: {},
    params: {},
    config: {
      pageSize: 24,
      pageMode: 'infinite_scroll',
    },
    status: 'initial',
    isNoResults: false,
    fromNoResultsWithFilters: false,
  }),
  
  getters: {
    request,  // Computed search request
    query,    // Current query
  },
  
  mutations: {
    appendResults(state, results) {
      state.results = [...state.results, ...results]
    },
    resetState(state) {
      Object.assign(state, resettableState())
    },
    setQuery,
    setResults(state, results) {
      state.results = results
    },
    setFacets(state, facets) {
      state.facets = facets
    },
    // ... 20+ more mutations
  },
  
  actions: {
    cancelFetchAndSaveSearchResponse,
    fetchSearchResponse,
    fetchAndSaveSearchResponse,
    increasePageAppendingResults,
    resetRequestOnRefinement,
    saveSearchResponse,
    setUrlParams,
    saveOrigin,
  },
}
interface SearchState {
  query: string
  results: Result[]
  partialResults: PartialResult[]
  facets: Facet[]
  relatedTags: RelatedTag[]
  banners: Banner[]
  promoteds: Promoted[]
  totalResults: number
  spellcheckedQuery: string
  sort: string
  page: number
  origin: Origin | null
  isAppendResults: boolean
  selectedFilters: Dictionary<Filter[]>
  params: Dictionary<unknown>
  config: SearchConfig
  status: Status
  isNoResults: boolean
  fromNoResultsWithFilters: boolean
  redirections: Redirection[]
  queryTagging: TaggingInfo
  displayTagging: TaggingInfo
  stats: Stats
}

State Access

From Components

Access state using Vuex’s mapState or computed properties:
<template>
  <div>
    <p>Query: {{ query }}</p>
    <p>Results: {{ results.length }}</p>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState('x/search', ['query', 'results']),
    
    // Or manually
    query() {
      return this.$store.state.x.search.query
    },
  },
}
</script>
Use the useState composable in Composition API for reactive state access.

From Store Actions

Actions receive context with typed state access:
actions: {
  async fetchAndSaveSearchResponse({ state, commit, dispatch, getters }) {
    // Access state
    const { query, page } = state
    
    // Access getters
    const request = getters.request
    
    // Commit mutations
    commit('setStatus', 'loading')
    
    // Call other actions
    await dispatch('saveSearchResponse', response)
  },
}

Getters

Getters compute derived state from the store:
// From: x-modules/search/store/getters/request.getter.ts
export const request: SearchGetter<InternalSearchRequest | null> = (
  state,
  getters,
) => {
  // Only return request if query is not empty
  if (!state.query) {
    return null
  }
  
  return {
    query: state.query,
    rows: state.config.pageSize,
    start: (state.page - 1) * state.config.pageSize,
    filters: Object.values(state.selectedFilters).flat(),
    sort: state.sort,
    extraParams: state.params,
  }
}

Accessing Getters

<template>
  <div>
    <p>{{ searchRequest }}</p>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters('x/search', ['request']),
    
    // Or use namespace helpers
    ...mapGetters('x/search', {
      searchRequest: 'request',
    }),
  },
}
</script>

Mutations

Mutations are the only way to modify state. They must be synchronous:
mutations: {
  // Simple mutation
  setQuery(state, query: string) {
    state.query = query
  },
  
  // Mutation with object payload
  setResults(state, results: Result[]) {
    state.results = results
  },
  
  // Mutation modifying nested state
  setSelectedFilters(state, selectedFilters: Filter[]) {
    state.selectedFilters = groupItemsBy(
      selectedFilters,
      filter => isFacetFilter(filter) ? filter.facetId : UNKNOWN_FACET_KEY
    )
  },
  
  // Mutation resetting state
  resetState(state) {
    Object.assign(state, resettableState())
  },
  
  // Conditional reset
  resetStateForReload(state) {
    const { query, facets, sort, page, ...resettable } = resettableState()
    Object.assign(state, resettable)
  },
}

Committing Mutations

import { wireCommit } from '@empathyco/x-components/wiring'

const searchWiring = createWiring({
  UserAcceptedAQuery: {
    setSearchQuery: wireCommit('x/search/setQuery'),
  },
})
Never mutate state directly outside of mutations. Always use commit() to ensure reactivity and enable time-travel debugging.

Actions

Actions handle asynchronous operations and can commit multiple mutations:
// From: x-modules/search/store/actions/fetch-and-save-search-response.action.ts
export const fetchAndSaveSearchResponse: SearchAction<
  'fetchAndSaveSearchResponse',
  void
> = async ({ state, commit, dispatch, getters }) => {
  const request = getters.request
  
  if (!request) {
    return
  }
  
  commit('setStatus', 'loading')
  
  try {
    const response = await XPlugin.adapter.search(request)
    
    if (state.isAppendResults) {
      commit('appendResults', response.results)
    } else {
      commit('setResults', response.results)
    }
    
    commit('setFacets', response.facets)
    commit('setTotalResults', response.totalResults)
    commit('setBanners', response.banners)
    commit('setPromoteds', response.promoteds)
    commit('setSpellcheck', response.spellcheck)
    commit('setStatus', 'success')
    
  } catch (error) {
    commit('setStatus', 'error')
    throw error
  }
}

Action Composition

Actions can call other actions:
actions: {
  async increasePageAppendingResults({ state, commit, dispatch }) {
    commit('setIsAppendResults', true)
    commit('setPage', state.page + 1)
    
    // Dispatch another action
    await dispatch('fetchAndSaveSearchResponse')
  },
  
  async setUrlParams({ commit, dispatch }, params) {
    commit('setQuery', params.query)
    commit('setPage', params.page ?? 1)
    commit('setSort', params.sort ?? '')
    
    // Fetch with new params
    await dispatch('fetchAndSaveSearchResponse')
  },
}

Dispatching Actions

import { wireDispatch } from '@empathyco/x-components/wiring'

const searchWiring = createWiring({
  SearchRequestUpdated: {
    fetchAndSaveSearchResponseWire: wireDispatch(
      'x/search/fetchAndSaveSearchResponse'
    ),
  },
})

Store Emitters

Store emitters watch state changes and emit events automatically:
// From: x-modules/search/store/emitters.ts
export const searchEmitters = createStoreEmitters(searchXStoreModule, {
  // Simple emitter - emits when results change
  ResultsChanged: state => state.results,
  
  // Emitter using getters
  SearchRequestUpdated: (_, getters) => getters.request,
  
  // Emitter with filter
  FacetsChanged: {
    selector: state => state.facets,
    filter(newValue, oldValue) {
      // Only emit if there are facets or there were facets
      return newValue.length !== 0 || oldValue.length !== 0
    },
  },
  
  // Complex emitter
  SearchResponseChanged: {
    selector: (state, getters) => ({
      request: getters.request!,
      status: state.status,
      banners: state.banners,
      facets: state.facets,
      partialResults: state.partialResults,
      promoteds: state.promoteds,
      queryTagging: state.queryTagging,
      displayTagging: state.displayTagging,
      redirections: state.redirections,
      results: state.results,
      spellcheck: state.spellcheckedQuery,
      totalResults: state.totalResults,
    }),
    filter: (newValue, oldValue) => {
      // Only emit when loading is complete
      return (
        newValue.status !== oldValue.status &&
        oldValue.status === 'loading' &&
        !!newValue.request
      )
    },
  },
  
  // Emitter with metadata
  SearchTaggingChanged: {
    selector: state => state.queryTagging,
    filter: ({ url }) => !isStringEmpty(url),
    metadata: {
      moduleName: 'search',
    },
  },
})
Store emitters create the reactive bridge between Vuex state and the event bus. They’re registered automatically when a module is installed.

How Emitters Work

Type Extraction Utilities

Interface X provides utilities to extract types from modules:
import type {
  ExtractState,
  ExtractGetters,
  ExtractMutations,
  ExtractActions,
  ExtractMutationPayload,
  ExtractActionPayload,
} from '@empathyco/x-components'

// Extract state type
type SearchState = ExtractState<'search'>

// Extract getters type
type SearchGetters = ExtractGetters<'search'>

// Extract mutations type
type SearchMutations = ExtractMutations<SearchXModule>

// Extract actions type
type SearchActions = ExtractActions<SearchXModule>

// Extract specific mutation payload
type SetQueryPayload = ExtractMutationPayload<'search', 'setQuery'>
// Result: string

// Extract specific action payload
type FetchPayload = ExtractActionPayload<'search', 'fetchAndSaveSearchResponse'>
// Result: void

Store Configuration

Configure module stores when installing XPlugin:
import { createApp } from 'vue'
import { xPlugin } from '@empathyco/x-components'
import { createStore } from 'vuex'

const app = createApp(App)

// Option 1: Let XPlugin create the store
app.use(xPlugin, {
  adapter: platformAdapter,
  xModules: {
    search: {
      config: {
        pageSize: 48,
        pageMode: 'paginated',
      },
    },
  },
})

// Option 2: Provide your own store
const myStore = createStore({
  // Your modules
})

app.use(xPlugin, {
  adapter: platformAdapter,
  store: myStore,  // X modules will be registered under 'x' namespace
})

Resettable State Pattern

The search module demonstrates a useful pattern for resetting state:
// Define resettable state as a function
function resettableState() {
  return {
    query: '',
    results: [],
    partialResults: [],
    // ... more fields that should reset
  }
}

export const searchXStoreModule = {
  state: () => ({
    ...resettableState(),
    // Persistent fields that don't reset
    selectedFilters: {},
    config: { pageSize: 24 },
  }),
  
  mutations: {
    // Full reset
    resetState(state) {
      Object.assign(state, resettableState())
    },
    
    // Partial reset (keep some fields)
    resetStateForReload(state) {
      const { query, facets, sort, page, ...resettable } = resettableState()
      Object.assign(state, resettable)
    },
  },
}
Use this pattern when you need to reset parts of your state while keeping other parts intact (like user preferences or config).

Accessing Root State

While modules should be self-contained, you can access root state when needed:
actions: {
  async myAction({ rootState, rootGetters }) {
    // Access other module state
    const facets = rootState.x.facets.facets
    const searchQuery = rootState.x.search.query
    
    // Access other module getters
    const searchRequest = rootGetters['x/search/request']
  },
}
Accessing other module state creates coupling. Prefer communicating through events instead.

Best Practices

Mutations should only modify state, not contain business logic:
  • state.query = query
  • state.results = results
  • ❌ API calls
  • ❌ Complex computations
  • ❌ Emitting events
All asynchronous operations belong in actions:
  • ✅ API calls
  • ✅ Multiple mutation commits
  • ✅ Dispatching other actions
  • ✅ Complex business logic
Don’t duplicate state - compute it in getters:
  • totalItems: state => state.items.length
  • filteredItems: state => state.items.filter(...)
  • ❌ Storing derived data in state
Take advantage of TypeScript:
  • ✅ Define state interfaces
  • ✅ Type mutation payloads
  • ✅ Type action payloads
  • ✅ Use type extraction utilities

Next Steps

Architecture Overview

See how stores fit into the overall architecture

Event System

Learn how stores emit events automatically

Build docs developers (and LLMs) love