- [Dependency Injection](#dependency-injection) - [The `createContext` Factory](#the-createcontext-factory) - [When to Use DI](#when-to-use-di) - [Platform Abstraction (Primary Use Case)](#platform-abstraction-primary-use-case) - [Page-Level Context](#page-level-context) - [Creating a New Provider](#creating-a-new-provider) - [1. Define the interface in `packages/ui/src/providers/`](#1-define-the-interface-in-packagesuisrcproviders) - [2. For complex platform-specific logic, use an abstract class](#2-for-complex-platform-specific-logic-use-an-abstract-class) - [Wiring Up Providers](#wiring-up-providers) - [App Frontend (Tauri)](#app-frontend-tauri) - [Website Frontend (Nuxt)](#website-frontend-nuxt) - [Consuming Providers](#consuming-providers) - [When NOT to Use DI](#when-not-to-use-di) - [Existing Providers](#existing-providers) - [Key Files](#key-files) # Dependency Injection Modrinth uses a lightweight DI layer built on Vue's `provide`/`inject` for sharing platform-specific capabilities and page-level state across shared UI components. ## The `createContext` Factory All providers are defined using `createContext` from `packages/ui/src/providers/index.ts` (adapted from Reka UI). It produces a typed `[inject, provide]` tuple: ```ts import { createContext } from '@modrinth/ui' interface MyContext { someValue: Ref doSomething: () => void } export const [injectMyContext, provideMyContext] = createContext('MyComponent') ``` - **`provideMyContext(value)`** — call in a parent component's `setup()`. - **`injectMyContext()`** — call in any descendant's `setup()`. Throws if never provided. - **`injectMyContext(null)`** — returns `null` instead of throwing (for optional contexts). ## When to Use DI Use DI when: - **The same interface needs different implementations** depending on the platform (web vs desktop app). - **Deeply nested components** need access to shared page-level state without prop drilling through 3+ levels. ### Platform Abstraction (Primary Use Case) `packages/ui` components need capabilities that each frontend fulfils differently: | Provider | App Frontend | Website Frontend | | ------------- | -------------------------------- | ------------------------------ | | API client | Tauri IPC client | REST fetch client | | Notifications | `ref()` state + app window mgmt | `useState()` for SSR hydration | | File picker | Native Tauri dialogs | Browser file inputs | | Tags | Tauri commands | Nuxt server state | | Page context | `sidebar: true`, ad window hooks | `sidebar: false`, no ads | ### Page-Level Context Sharing data between a page and deeply nested children — e.g. project page data consumed by sidebar, header, and version components. ## Creating a New Provider ### 1. Define the interface in `packages/ui/src/providers/` ```ts // packages/ui/src/providers/my-feature.ts import type { Ref } from 'vue' import { createContext } from '.' export interface MyFeatureContext { items: Ref addItem: (item: Item) => Promise removeItem: (id: string) => Promise } export const [injectMyFeature, provideMyFeature] = createContext('MyFeature') ``` Re-export from the barrel file (`packages/ui/src/providers/index.ts`). ### 2. For complex platform-specific logic, use an abstract class ```ts export abstract class AbstractMyFeatureManager { abstract items: Ref abstract addItem(item: Item): Promise // Shared logic lives on the base class handleError(err: unknown) { console.error(err) } } export const [injectMyFeature, provideMyFeature] = createContext('MyFeature') ``` See `AbstractWebNotificationManager` in `packages/ui/src/providers/web-notifications.ts` for a real example. ## Wiring Up Providers ### App Frontend (Tauri) Create a setup function in `apps/app-frontend/src/providers/setup/`: ```ts // apps/app-frontend/src/providers/setup/my-feature.ts import { ref } from 'vue' import { provideMyFeature } from '@modrinth/ui' export function setupMyFeatureProvider() { const items = ref([]) provideMyFeature({ items, addItem: async (item) => { await invoke('add_item', { item }) items.value.push(item) }, removeItem: async (id) => { await invoke('remove_item', { id }) items.value = items.value.filter(i => i.id !== id) }, }) } ``` Register it in `apps/app-frontend/src/providers/setup.ts`, which is called from `App.vue`'s `setup()`. ### Website Frontend (Nuxt) Provide directly in `apps/frontend/src/app.vue`, using Nuxt's `useState()` where SSR hydration is needed: ```ts provideMyFeature({ items: useState('my-feature-items', () => []), addItem: async (item) => { await $fetch('/api/items', { method: 'POST', body: item }) }, removeItem: async (id) => { await $fetch(`/api/items/${id}`, { method: 'DELETE' }) }, }) ``` ## Consuming Providers In any component across `packages/ui`, `apps/frontend`, or `apps/app-frontend`: ```vue ``` ## When NOT to Use DI Default to props and emits. DI adds indirection — only use it with a concrete reason. - **Parent to direct child** — use props. - **Data only exists in one frontend** — keep context local to that app, not in `packages/ui`. - **Shallow prop drilling (1–2 levels)** — passing through one intermediate is fine. - **Component-local state** — use `ref()` / `reactive()` locally. ## Existing Providers | Provider | File | Purpose | | ---------------------------- | -------------------------------- | ------------------------------ | | `provideModrinthClient` | `providers/api-client.ts` | API client instance | | `provideNotificationManager` | `providers/web-notifications.ts` | Notification management | | `providePageContext` | `providers/page-context.ts` | Page config (sidebar, ads) | | `provideProjectPageContext` | `providers/project-page.ts` | Project page state + mutations | | `provideServerContext` | `providers/server-context.ts` | Server hosting state | | `provideUserPageContext` | `providers/user-page.ts` | User page state | ## Key Files - `packages/ui/src/providers/index.ts` — `createContext` factory + barrel exports - `packages/ui/src/providers/*.ts` — Provider definitions - `apps/frontend/src/app.vue` — Nuxt root provider setup - `apps/app-frontend/src/App.vue` — Tauri root provider setup - `apps/app-frontend/src/providers/setup/` — App provider setup functions