Composables
Composables are reusable functions that encapsulate stateful logic using Vue’s Composition API. They are the preferred approach for state management in Kolibri, replacing Vuex (which is deprecated).
They follow the naming convention use* (e.g., useChannels, useTaskPolling).
Note
The Composition API is the preferred approach for all new Vue code in Kolibri, not just composables. New components should use setup() rather than the Options API. Existing Options API components do not need to be migrated, but new code should follow Composition API patterns.
See also
For information about Vuex deprecation, see Vuex.
Key benefits:
Reusability: Share logic across components
Testability: Easy to test in isolation
Simplicity: No boilerplate compared to Vuex stores
When to Use Composables
Use composables when you need to:
Share state or logic between components
Encapsulate complex stateful logic
Implement reusable behaviors (e.g., polling, route tracking)
Do not use Vuex - it is deprecated. See Vuex for migration guidance.
Composable Conventions
File naming and location
Name:
use*.js(e.g.,useChannels.js)Location:
Shared composables:
packages/kolibri-common/composables/Plugin-specific:
kolibri/plugins/<plugin>/frontend/composables/Component-specific: Co-located with component
Function structure
/**
* A composable function containing logic related to [feature]
*/
import { ref, computed } from 'vue';
export default function useFeatureName() {
// Reactive state
const items = ref([]);
const isLoading = ref(false);
// Computed properties
const itemCount = computed(() => items.value.length);
// Methods
function addItem(item) {
items.value.push(item);
}
// Return public API
return {
items,
isLoading,
itemCount,
addItem,
};
}
Include JSDoc comments describing the composable’s purpose.
Provider/Inject Pattern
For state scoped to a component tree (not truly global), use Vue’s provide/inject:
import { ref, provide, inject } from 'vue';
/**
* Composable for providing a route tracking context
*/
export default function usePreviousRoute() {
const previousRoute = ref(null);
// Provide to child components
provide('previousRoute', previousRoute);
return previousRoute;
}
/**
* Inject the previous route ref
*/
export function injectPreviousRoute() {
return inject('previousRoute');
}
This keeps state scoped to a specific component hierarchy rather than global.
Testing Composables
Tests for composables live in __tests__/ directories following Jest conventions:
composables/
├── __tests__/
│ └── useChannels.spec.js
└── useChannels.js
Example test structure:
import { ref } from 'vue';
import useChannels from '../useChannels';
describe('useChannels', () => {
beforeEach(() => {
// Setup mocks
jest.clearAllMocks();
});
it('should fetch channels', async () => {
const { fetchChannels, channelsMap } = useChannels();
await fetchChannels();
expect(Object.keys(channelsMap).length).toBeGreaterThan(0);
});
});
See Unit testing for comprehensive testing guidance.
Common Composable Patterns
Resource fetching
import { ref } from 'vue';
import ResourceAPI from './api';
export default function useResource() {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
async function fetch(id) {
loading.value = true;
error.value = null;
try {
data.value = await ResourceAPI.get(id);
} catch (e) {
error.value = e;
} finally {
loading.value = false;
}
}
return { data, loading, error, fetch };
}
Polling
import { ref, onMounted, onUnmounted } from 'vue';
import { useTimeoutPoll } from '@vueuse/core';
export default function useTaskPolling(queueName) {
const tasks = ref([]);
const { pause, resume } = useTimeoutPoll(
async () => {
tasks.value = await fetchTasks(queueName);
},
5000,
{ immediate: true }
);
onMounted(() => resume());
onUnmounted(() => pause());
return { tasks };
}
Lifecycle management
Composables can use lifecycle hooks just like components:
import { onMounted, onUnmounted } from 'vue';
export default function useEventListener(target, event, handler) {
onMounted(() => {
target.addEventListener(event, handler);
});
onUnmounted(() => {
target.removeEventListener(event, handler);
});
}
Using @vueuse/core
Kolibri uses utilities from @vueuse/core which provides many useful composables:
useTimeoutPoll- Polling with automatic cleanupget/set- Safe reactive ref accessAnd many more utilities
Always check @vueuse/core before implementing common patterns yourself.
Migration from Vuex
Vuex is deprecated in favor of composables. Here’s a quick comparison:
Vuex |
Composables |
|---|---|
|
|
|
|
|
|
|
|
Best Practices
Keep composables focused: Each composable should have a single, clear purpose
Use module-level state sparingly: Most state should be local to components
Document with JSDoc: Always include function and parameter documentation
Return consistent interface: Return an object with clear, named properties
Handle cleanup: Use
onUnmountedfor cleanup (event listeners, timers, etc.)Prefer composition over inheritance: Compose multiple small composables rather than creating large ones
Test in isolation: Write unit tests for composables separate from components
Examples in the Codebase
Good examples to reference:
packages/kolibri-common/composables/useChannels.js- Shared state patternpackages/kolibri-common/composables/useTaskPolling.js- Polling patternpackages/kolibri-common/composables/usePreviousRoute.js- Provider/inject patternpackages/kolibri-common/composables/useBaseSearch.js- Complex state management
Browse packages/kolibri-common/composables/ for more examples.