Page MenuHomePhabricator

[26Q2] Use Vue 3 Composition API in ext.wikilambda.app
Closed, ResolvedPublic

Description

Migrate WikiLambda Vue Frontend to Composition API so that the codebase is more maintainable, modern, and easier to extend with improved type safety and logic reuse. Also it is in line with how Codex uses Vue 3.

Acceptance Criteria

  • All Vue components use the Composition API in their <script> blocks.
  • Shared logic previously in mixins is refactored into composable functions in the composables/ directory.
  • Pinia stores might initially not need any updates, because they should work with both Composition and Options API. We do need to update the usage of the store in the components.
  • The application builds and runs without errors or warnings.
  • All existing tests pass.
  • No linting errors
  • Documentation is updated to reflect the new patterns and best practices.
  • (not possible) All JavaScript and Vue files use ES module syntax (import/export) instead of CommonJS (require/module.exports).

📖 Migration Guide

Migration types

Composables (9 available)
// In your component setup():
const { proxy } = getCurrentInstance();

// Error handling
const errors = useError( { keyPath: props.keyPath } );

// Clipboard operations
const clipboard = useClipboard( );

// Event logging
const eventLog = useEventLog();

// Type utilities
const typeUtils = useType();

// ZObject access
const zobject = useZObject( { keyPath: props.KeyPath } );

// Page title management
const pageTitle = usePageTitle();

// Scroll behavior
const scroll = useScroll();

// Metadata configuration
const metadata = useMetadata();
Stores

All stores accessible through useMainStore():

Per Pinia docs, use storeToRefs() to destructure state and getters while maintaining reactivity:

Key Rules

  1. Destructure getters/state with storeToRefs() - Maintains reactivity
  2. Destructure actions directly from store - They're just functions, no reactivity needed
  3. Or access everything via store.property - Simpler, always works

Why? Pinia getters and state are reactive refs. Direct destructuring without storeToRefs() breaks reactivity. Actions are plain functions and can be destructured directly.

setup( props, { emit } ) {
  const store = useMainStore();

  // ✅ CORRECT: Use storeToRefs when destructuring getters/state
  const { isInitialized, getCurrentView } = storeToRefs( store );

  // ✅ CORRECT: Actions can be destructured directly (they're just functions)
  const { setValueByKeyPath, fetchZids } = store;

  // ✅ CORRECT: Access store directly in computed/methods
  const displayValue = computed( () => store.getLabelData( someId ) );

  function handleClick() {
    setValueByKeyPath( value );  // ✅ Destructured action
    store.fetchZids( zids );     // ✅ Or call directly
  }

  return {
    // Reactive refs from storeToRefs
    isInitialized,
    getCurrentView,
    // Other data
    displayValue,
    handleClick
  };
}
Components

Key Rules

DO:

  • Use single <script> tag
  • Use setup( props, { emit } ) inside defineComponent()
  • Access props via props.propName
  • Use emit() for events
  • Use inject( 'i18n' ) for internationalization (MediaWiki provides i18n via app.provide)
  • Access store via store.property (e.g., store.getLabelData, store.setValueByKeyPath())
  • Use storeToRefs() ONLY for getters returned by reference in the return statement
  • Explicitly return all values/methods template needs
  • Destructure only needed methods from composables

DON'T:

  • Use two <script> tags
  • Use spread operators in returns
  • Use this (use props/emit instead)
  • Use getCurrentInstance() for i18n (use inject( 'i18n' ) instead)
  • Destructure from store (can cause reactivity issues and test complications)
  • Return everything (only what template needs)
vue
<script>
const { computed, defineComponent, inject, ref } = require( 'vue' );
const useZObject = require( '../../composables/useZObject.js' );
const useMainStore = require( '../../store/index.js' );

module.exports = exports = defineComponent( {
  name: 'my-component',
  components: {
    'child-component': ChildComponent
  },
  props: {
    keyPath: {
      type: String,
      required: true
    },
    objectValue: {
      type: Object,
      required: true
    }
  },
  emits: [ 'update-value' ],
  setup( props, { emit } ) {
    // 1. Inject i18n (provided by MediaWiki)
    const i18n = inject( 'i18n' );

    // 2. Use composables (destructure only what's needed)
    const { getZObjectType, getZBooleanValue } = useZObject();

    // 3. Access store (DO NOT destructure - use store.property)
    const store = useMainStore();

    // 4. Reactive state
    const localState = ref( 'initial' );

    // 5. Computed properties
    const displayValue = computed( () => getZObjectType( props.objectValue ) );

    // 6. Methods
    function handleUpdate( value ) {
      store.setValueByKeyPath( value );  // Use store.action()
      emit( 'update-value', value );
    }

    // 7. MUST explicitly return everything the template uses
    return {
      // Only return what template actually uses
      getZObjectType,
      displayValue,
      handleUpdate,
      // Expose store getters/actions used in template
      getLabelData: store.getLabelData
    };
  }
} );
</script>

Common Conversions

From (Options API)To (Composition API)
data() { return { x: 0 } }const x = ref( 0 )
computed: { y() { return this.x * 2 } }const y = computed( () => x.value * 2 )
methods: { doIt() {} }function doIt() {}
mounted() {}onMounted( () => {} )
this.xx.value (for refs)
this.$i18nproxy.$i18n (via getCurrentInstance)
mapState( store, ['x'] )const { x } = storeToRefs( store )
mapActions( store, ['doIt'] )const { doIt } = store
mixins: [ myMixin ]const mixin = useMyComposable()

🗺️ Conversion Roadmap 🤖

Phase 1: Mixins → Composables (8/8) ✅

All mixins should be converted to composables in resources/ext.wikilambda.app/composables/:

Old MixinNew ComposablePurpose
clipboardMixin.jsuseClipboard.jsClipboard operations
errorMixin.jsuseError.jsError handling
eventLogMixin.jsuseEventLog.jsEvent logging
metadataMixin.jsuseMetadata.jsMetadata configuration
pageTitleMixin.jsusePageTitle.jsPage title management
scrollMixin.jsuseScroll.jsScroll behavior
typeMixin.jsuseType.jsType utilities
zobjectMixin.jsuseZObject.jsZObject access

Est. Total: 1-2 days

Phase 2. Pinia Store Modules → Import to Composition API

The stores import should be converted to Composition api style using useMainStore and setup()

Phase 3: Component Migration

  • Convert all Vue components to Composition API
  • Update component tests

Batch Convert ALL Components

  1. Week 1: Base + Widget components (~27 components, Batches 1-2)
    • Days 1-2: Base components (12) - Foundation for all others
    • Days 3-5: Widget components (15) - UI controls
  1. Week 2: Function + Specialized + View components (~56 components, Batches 3-5)
    • Days 1-3: Function components (20) - Core functionality
    • Days 4-5: Specialized + View components (36) - Final push

AI Workflow:

  • Use AI to analyze existing converted components as patterns
  • Manual review and testing of critical paths

Est. Total: 10-20 days (2/4 weeks)

Phase 4: Final Cleanup, Testing, quality and cleanup

  1. Full integration testing (~1 day)
  2. Performance optimization & bug fixes (~1 day)
  3. Documentation updates (ongoing)
  4. Update all Jest tests that are still broken
  5. Fix linting errors
  6. Run full test suite
  7. Update documentation [TODO]

Est. Total: 3-5 days

Learnings and Follow-up work

Learnings

  • Composition API groups logic by feature, not by option type. We did not do this for the sake of review but we should do this as follow-up work.
  • Composition API integrates naturally with TypeScript because everything is just JavaScript functions and variables. Is it possible for us to consider this?
  • Ran into a lot of nextTick usage which is usually not needed. removed these
  • Some html elements do not have the correct semantic html element (like using an a for a button), we should perhaps go through the codebase and fix those occasions. Perhaps also do an accessibility check for these.
  • For unit testing, we want to place greater emphasis on user-focused behavior testing, ensuring our components work the way users actually interact with them.

Follow-up work

Details

Event Timeline

DSmit-WMF updated the task description. (Show Details)
DSmit-WMF updated the task description. (Show Details)
DSmit-WMF updated the task description. (Show Details)
DSmit-WMF updated the task description. (Show Details)
Jdforrester-WMF renamed this task from Use Vue 3 Composition API in ext.wikilambda.app when possible to [26Q2] Use Vue 3 Composition API in ext.wikilambda.app.Oct 3 2025, 3:16 PM
Jdforrester-WMF assigned this task to DSmit-WMF.

@DSmit-WMF: This is the Epic and probably shouldn't itself be in progress, but have sub-tasks underneath it.

We can probably also simplify the useClipboard composable to something like this:

import { ref, computed } from 'vue'

export function useClipboard(timeout = 2000) {
  const copiedText = ref<string | null>(null)
  const isCopied = computed(() => copiedText.value !== null)

  async function copy(text: string) {
    try {
      await navigator.clipboard.writeText(text)
      copiedText.value = text

      // Reset after timeout
      setTimeout(() => {
        if (copiedText.value === text) copiedText.value = null
      }, timeout)
    } catch (err) {
      console.error('Failed to copy:', err)
    }
  }

  return {
    copy,
    isCopied,
    copiedText,
  }
}
DSmit-WMF updated the task description. (Show Details)
DSmit-WMF updated the task description. (Show Details)
DSmit-WMF updated the task description. (Show Details)
DSmit-WMF updated the task description. (Show Details)
DSmit-WMF updated the task description. (Show Details)

Change #1207168 had a related patch set uploaded (by Genoveva Galarza; author: Genoveva Galarza):

[mediawiki/extensions/WikiLambda@master] Fix FunctionInputParser flaky test

https://gerrit.wikimedia.org/r/1207168

Change #1207168 merged by jenkins-bot:

[mediawiki/extensions/WikiLambda@master] Fix FunctionInputParser flaky test

https://gerrit.wikimedia.org/r/1207168

DSmit-WMF changed the task status from Open to In Progress.Nov 20 2025, 4:17 PM