Skip to main content
X Modules are the fundamental building blocks of Interface X. Each module encapsulates a complete feature - including its state management, event handling, and UI components - into a self-contained, reusable unit.

What is an X Module?

An X Module is a TypeScript object that brings together four key pieces:
export interface XModule<StoreModule extends AnyXStoreModule> {
  /** A unique name that identifies this XModule */
  name: XModuleName
  
  /** Watchers for the store module that will emit an XEvent when changed */
  storeEmitters: StoreEmitters<StoreModule>
  
  /** The Vuex Store module associated to this module */
  storeModule: StoreModule
  
  /** The wiring associated to this module */
  wiring: Partial<Wiring>
}
Every X Module must have these four properties. Together, they define the module’s complete behavior.

Module Structure

Let’s examine the search module as a real example from the codebase:
x-modules/search/
├── components/          # Vue components for this module
│   ├── SearchInput.vue
│   ├── ResultsList.vue
│   └── ...
├── store/              # Vuex store module
│   ├── module.ts       # State, mutations, actions, getters
│   ├── emitters.ts     # Store emitters configuration
│   ├── actions/        # Action implementations
│   ├── getters/        # Getter implementations
│   └── types.ts        # TypeScript types
├── wiring.ts           # Event wiring configuration
├── events.types.ts     # Module-specific event types
├── config.types.ts     # Configuration options
└── x-module.ts         # Module definition and registration

Module Definition

Here’s the actual search module definition from the source code:
// x-modules/search/x-module.ts
import type { XModule } from '../x-modules.types'
import type { SearchXStoreModule } from './store/types'
import { XPlugin } from '../../plugins/x-plugin'
import { searchEmitters } from './store/emitters'
import { searchXStoreModule } from './store/module'
import { searchWiring } from './wiring'

export type SearchXModule = XModule<SearchXStoreModule>

export const searchXModule: SearchXModule = {
  name: 'search',
  storeModule: searchXStoreModule,
  storeEmitters: searchEmitters,
  wiring: searchWiring,
}

// Auto-register on import
XPlugin.registerXModule(searchXModule)
The XPlugin.registerXModule() call at the bottom is crucial - it auto-registers the module when any component from it is imported.

Available X Modules

Interface X ships with 22 built-in modules covering all aspects of search and discovery:
  • search - Main search functionality, results, and pagination
  • search-box - Search input with query handling
  • facets - Filtering with hierarchical and range facets
  • query-suggestions - Autocomplete suggestions as users type
  • popular-searches - Trending and popular queries
  • recommendations - Product recommendations
  • next-queries - Related searches and query refinement
  • related-tags - Tag-based navigation
  • semantic-queries - Semantic search capabilities
  • related-prompts - AI-driven query prompts
  • empathize - Combined empathy layer (suggestions, popular searches, etc.)
  • history-queries - Search history management
  • identifier-results - Product identification
  • queries-preview - Preview results for queries
  • scroll - Infinite scroll and pagination
  • url - URL parameter synchronization
  • tagging - Analytics and tracking
  • experience-controls - A/B testing and personalization
  • extra-params - Additional request parameters
  • device - Device detection
  • ai - AI-powered features

Module Anatomy Deep Dive

1. Store Module

Each module has a type-safe Vuex store module:
// From: x-modules/search/store/module.ts
export const searchXStoreModule: SearchXStoreModule = {
  state: () => ({
    query: '',
    results: [],
    facets: [],
    totalResults: 0,
    page: 1,
    sort: '',
    status: 'initial',
    config: {
      pageSize: 24,
      pageMode: 'infinite_scroll',
    },
    // ... more state
  }),
  
  getters: {
    request(state, getters) {
      // Compute search request from state
    },
    query(state) {
      return state.query
    },
  },
  
  mutations: {
    setQuery(state, query) {
      state.query = query
    },
    setResults(state, results) {
      state.results = results
    },
    // ... more mutations
  },
  
  actions: {
    async fetchAndSaveSearchResponse({ state, commit, dispatch }) {
      commit('setStatus', 'loading')
      const response = await adapter.search(/* ... */)
      commit('setResults', response.results)
      commit('setStatus', 'success')
    },
    // ... more actions
  },
}
All module stores are automatically namespaced under x.[moduleName] when registered. For example, the search state is at store.state.x.search.

2. Store Emitters

Store emitters watch for state changes and emit events:
// From: x-modules/search/store/emitters.ts
export const searchEmitters = createStoreEmitters(searchXStoreModule, {
  // Emit when results change
  ResultsChanged: state => state.results,
  
  // Emit when search request is updated
  SearchRequestUpdated: (_, getters) => getters.request,
  
  // Emit with custom filter
  SearchResponseChanged: {
    selector: (state, getters) => ({
      request: getters.request,
      status: state.status,
      results: state.results,
      facets: state.facets,
      totalResults: state.totalResults,
    }),
    filter: (newValue, oldValue) => {
      // Only emit when response has finished loading
      return newValue.status !== oldValue.status && 
             oldValue.status === 'loading'
    },
  },
  
  // More emitters...
})
Store emitters create a reactive bridge between your Vuex state and the event system. When state changes, events are automatically emitted.

3. Wiring

Wiring connects events to actions:
// From: x-modules/search/wiring.ts
export const searchWiring = createWiring({
  // When user accepts a query
  UserAcceptedAQuery: {
    setSearchQuery: wireCommit('setQuery'),
    saveOriginWire: wireDispatch('saveOrigin', ({ metadata }) => metadata),
  },
  
  // When search request is updated
  SearchRequestUpdated: {
    resetStateIfNoRequestWire: filterTruthyPayload(
      wireCommitWithoutPayload('resetState')
    ),
    fetchAndSaveSearchResponseWire: wireDispatch('fetchAndSaveSearchResponse'),
  },
  
  // When user clears the query
  UserClearedQuery: {
    setSearchQuery: wireCommit('setQuery', ''),
    cancelFetchAndSaveSearchResponseWire: wireDispatchWithoutPayload(
      'cancelFetchAndSaveSearchResponse'
    ),
  },
  
  // When user reaches end of results
  UserReachedResultsListEnd: {
    increasePageAppendingResultsWire: wireDispatchWithoutPayload(
      'increasePageAppendingResults'
    ),
  },
  
  // ... more wiring
})
// Commits a mutation
wireCommit('setQuery')

// Commits with static payload
wireCommit('setQuery', '')

// Commits with computed payload
wireCommit('setQuery', ({ eventPayload }) => eventPayload.toUpperCase())

4. Module Name

All module names are defined in a central type:
// From: x-modules/x-modules.types.ts
export interface XModulesTree {
  device: DeviceXModule
  empathize: EmpathizeXModule
  extraParams: ExtraParamsXModule
  facets: FacetsXModule
  historyQueries: HistoryQueriesXModule
  identifierResults: IdentifierResultsXModule
  nextQueries: NextQueriesXModule
  popularSearches: PopularSearchesXModule
  queriesPreview: QueriesPreviewXModule
  querySuggestions: QuerySuggestionsXModule
  recommendations: RecommendationsXModule
  relatedPrompts: RelatedPromptsXModule
  relatedTags: RelatedTagsXModule
  scroll: ScrollXModule
  search: SearchXModule
  searchBox: SearchBoxXModule
  semanticQueries: SemanticQueriesXModule
  tagging: TaggingXModule
  url: UrlXModule
  experienceControls: ExperienceControlsXModule
  ai: AiXModule
}

export type XModuleName = keyof XModulesTree

Module Communication

Modules never communicate directly - they only communicate through events:
1

Search Box emits event

User types in search box, which emits UserAcceptedAQuery event
2

Search module responds

Search module’s wiring reacts to the event and fetches results
3

Search emits response event

Store emitter automatically emits SearchResponseChanged event
4

Other modules respond

Facets, tagging, and other modules can react to the response event
This approach has major benefits:

Zero Coupling

Modules don’t import or reference each other

Easy Testing

Test modules in isolation by emitting events

Hot Swapping

Replace or disable modules without affecting others

Extensibility

Add custom modules that listen to existing events

Creating a Custom Module

You can create your own X Module following the same pattern:
import { XPlugin } from '@empathyco/x-components'
import type { XModule } from '@empathyco/x-components/x-modules'

// 1. Define your store module
const myStoreModule = {
  state: () => ({ items: [] }),
  getters: { /* ... */ },
  mutations: { /* ... */ },
  actions: { /* ... */ },
}

// 2. Define store emitters
const myEmitters = createStoreEmitters(myStoreModule, {
  ItemsChanged: state => state.items,
})

// 3. Define wiring
const myWiring = createWiring({
  UserClickedSomething: {
    doSomething: wireDispatch('fetchItems'),
  },
})

// 4. Create and register module
const myCustomModule: XModule<typeof myStoreModule> = {
  name: 'myCustomModule',
  storeModule: myStoreModule,
  storeEmitters: myEmitters,
  wiring: myWiring,
}

XPlugin.registerXModule(myCustomModule)
Custom module names must be added to the XModulesTree type for full type safety. Otherwise, TypeScript won’t recognize your module.

Module Configuration

You can customize any module when installing XPlugin:
app.use(xPlugin, {
  adapter: platformAdapter,
  xModules: {
    search: {
      // Override default config
      config: {
        pageSize: 48,
        pageMode: 'paginated',
      },
      
      // Add or override wiring
      wiring: {
        UserAcceptedAQuery: {
          customWire: wireDispatch('myCustomAction'),
        },
      },
    },
  },
})

Next Steps

Event System

Learn how events flow through the X Bus

State Management

Deep dive into the Vuex store structure

Build docs developers (and LLMs) love