Skip to content

Composition

The $compose method allows you to share functionality across components by composing reusable modules. Components will react to composable modules' changing property values, and composable modules can also interact with components' lifecycle methods. This pattern is especially useful for encapsulating common behaviors like mouse tracking, keyboard shortcuts, or data fetching, etc.

Creating a Composable

A composable is a class that encapsulates reusable functionality:

javascript
// composable-mouse.js
import { observable } from '@peak-js/core'

export default class {
  constructor(component) {
    this.position = observable({ x: null, y: null })

    const recordPosition = (e) => {
      this.position.x = e.clientX
      this.position.y = e.clientY
    }

    component.$on('mounted', () => {
      document.addEventListener('pointermove', recordPosition)
    })

    component.$on('teardown', () => {
      document.removeEventListener('pointermove', recordPosition)
    })

    return this.position
  }
}

Using $compose in Components

Use $compose in your component's initialize() method:

html
<!-- components/mouse-reporter.html -->
<template>
  <div class="card">
    <h2>Mouse Position</h2>
    <div>x: <span x-text="mouse.x"></span></div>
    <div>y: <span x-text="mouse.y"></span></div>
    </div>
  </div>
</template>

<script>
import ComposableMouse from '../lib/composable-mouse.js'

export default class {
  initialize() {
    this.mouse = this.$compose(ComposableMouse)
  }
}
</script>

Composable Patterns

Data Fetching Composable

javascript
// composable-fetch.js
import { observable } from '../../peak.js'

export default class {
  constructor(component) {
    const state = observable({
      data: null,
      loading: false,
      error: null
    })

    state.fetch = async (url) => {
      state.loading = true
      state.error = null

      try {
        const response = await fetch(url)
        state.data = await response.json()
      } catch (error) {
        state.error = error.message
      } finally {
        state.loading = false
      }
    }

    return state
  }
}

Local Storage Composable

javascript
// composable-storage.js
import { observable } from '../../peak.js'

export default class {
  constructor(component) {
    this.get = (key, defaultValue = null) => {
      try {
        const value = localStorage.getItem(key)
        return value ? JSON.parse(value) : defaultValue
      } catch {
        return defaultValue
      }
    }

    this.set = (key, value) => {
      try {
        localStorage.setItem(key, JSON.stringify(value))
      } catch (error) {
        console.warn('Failed to save to localStorage:', error)
      }
    }

    this.remove = (key) => {
      localStorage.removeItem(key)
    }

    return {
      get: this.get,
      set: this.set,
      remove: this.remove
    }
  }
}

Using Multiple Composables

You can compose multiple pieces of functionality in a single component:

html
<script>
import ComposableMouse from '../lib/composable-mouse.js'
import ComposableFetch from '../lib/composable-fetch.js'
import ComposableStorage from '../lib/composable-storage.js'

export default class {
  initialize() {
    // mix and match composable functionality
    this.mouse = this.$compose(ComposableMouse)
    this.api = this.$compose(ComposableFetch)
    this.storage = this.$compose(ComposableStorage)

    // use composed functionality
    this.loadUserData()
  }

  async loadUserData() {
    const cached = this.storage.get('userData')
    if (cached) {
      this.userData = cached
    } else {
      await this.api.fetch('/api/user')
      if (this.api.data) {
        this.userData = this.api.data
        this.storage.set('userData', this.api.data)
      }
    }
  }
}
</script>