pinia-di: Use Pinia more flexibly!
🔥 🔥 A Better way to reuse stores.
DI(dependency-injection) for pinia. work with vue@3.
npm install pinia pinia-diflowchart TD
A{{"StoreProvider[AppStore, MessageStore]<br>InjectorA"}} --> B["CompoentApp<br>InjectorB<br>const appStore = useStore(AppStore)();<br>const messageStore = useStore(MessageStore)()"]
B --> C{{"StoreProvider[ChildStore]<br>InjectorC"}}
B --> D{{"StoreProvider[ChildStore]<br>InjectorD"}}
C --> E["ComponentChild<br>InjectorE<br>const appStore = useStore(AppStore)();<br>const childStore = useStore(ChildStore)();"]
D --> F["ComponentChild<br>InjectorF<br>const appStore = useStore(AppStore)();<br>const childStore = useStore(ChildStore)();"]
E --> G{{"StoreProvider[ChildStore]<br>InjectorG"}}
G --> H["ComponentChild<br>InjectorH<br>const appStore = useStore(AppStore)();<br>const childStore = useStore(ChildStore)();"]
Injector: Injects and provides stores in the component tree to current component or child components.Store Tree: Thestore treeis like thecomponent tree, each component get the store from the nearest parentInjector.StoreProvider: A component that usesInjectorto provide stores.Store Use: The return type of defineStore.Store: The return type of aStore Usefunction like useStore();Store Creator: A function that returns aStore Use.InjectionContext: The parameter that theStore Creatorwill receive.
A Store Creator is a creator function that returns a defineStore() result.
For example: the AppStore is a Store Creator, and the return of AppStore() is Store Use:
import { defineStore } from 'pinia';
import { InjectionContext } from 'pinia-di';
// Store Creator
export const AppStore = (ctx: InjectionContext) => {
return defineStore(ctx.useStoreId('App'), {
//...
});
}
// Store Use
const useAppStore = AppStore();
// Store
const appStore = useAppStore();getStore: Get another store that has been provided by the current injector or parent injector.
import { InjectionContext } from 'pinia-di';
import { OtherStore } from './stores/other';
export const AppStore = ({ getStore }: InjectionContext) => {
return defineStore('app', () => {
const state = reactive({});
const test = () => {
// the OtherStore must be provided by `current injector` or `parent injector`
const otherStore = getStore(OtherStore);
console.log(otherStore.xx);
};
return {
state,
test
}
});
}useStoreId: Because pinia uses id to identify one store, but our Store Creator is reusable, you can use the method useStoreId to generate the unique id.
import { InjectionContext } from 'pinia-di';
export const TestStore = ({ useStoreId }: InjectionContext) => {
// store id will be `test-1`, `test-2`, ...
return defineStore(useStoreId('test'), () => {
const state = reactive({});
return {
state
}
});
}onUnmounted: Bind a function that will be invoked when the store is unmounted.
import { InjectionContext } from 'pinia-di';
export const TestStore = ({ onUnmounted }: InjectionContext) => {
const useTestStore = defineStore(useStoreId('test'), () => {
const state = reactive({});
const dispose = async () => {
console.log('dispose');
};
const remove = onUnmounted(dispose);
// unbind the callback
// remove()
return {
state
}
});
return useTestStore;
}You can use composition api useProvideStores or component StoreProvider to provide stores.
<script lang="ts" setup>
import { createApp } from 'vue';
import { useProvideStores, useStore } from 'pinia-di';
import { TestStore } from '@/stores/testStore';
// the testStore is provided by parent injector
const testStore = useStore(TestStore);
// 'test' is the injector name that helps to debug
useProvideStores([TestStore], 'test');
// testStoreNew is provided by the `useProvideStores` above
const testStoreNew = useStore(TestStore);
</script>Use StoreProvider to provide stores.
🔥 Tip: Because the stores prop is only used once, if it changes after the component is mounted, the new store prop will be ignored.
🔥 If you want to conditionally provide different stores, you need to write different components to provide each self.
App.vue
<script setup>
import { StoreProvider } from 'pinia-di';
import { AppStore } from '@/stores/appStore';
import { TestStore } from '@/stores/testStore';
</script>
<template>
<!-- // 'app' is the injector name that help to debug -->
<StoreProvider :stores="[AppStore]" name="app">
<Main />
<div>
<StoreProvider :stores="[TestStore]">
<div>test a</div>
</StoreProvider>
<StoreProvider :stores="[TestStore]">
<div>test n</div>
</StoreProvider>
</div>
</StoreProvider>
</template>And, you can provide stores in the app.provide for the whole vue app.
pinia-di provides a helper function getProvideArgs to do this.
import { createApp } from 'vue';
import { getProvideArgs } from 'pinia-di';
import { AppStore } from '@/stores/appStore';
const app = createApp();
// 'app' is the injector name that help to debug
app.provide(...getProvideArgs([AppStore], 'app'));
app.mount('#app');Component.vue
<script setup>
import { useStore } from 'pinia-di';
import { AppStore } from '@/stores/appStore';
const appStore = useStore(AppStore);
</script>*** Tips: If you use a Singleton Store, you can't get InjectionContext when then store is created ***
stores/messageStore.ts
import { defineStore } from 'pinia';
export const MessageStore = (/* no `ctx: InjectionContext` */) => {
return defineStore('message'), {
state: {}
});
}
export const useMessageStore = MessageStore();Then, if you want to use the same store of messageStore for MessageStore, you will use the use flag when you provide stores.
App.vue
<script setup>
import { StoreProvider, useStore } from 'pinia-di';
import { AppStore } from '@/stores/appStore';
import { useMessageStore, MessageStore } from '@/stores/messageStore';
const stores = [
AppStore, { creator: MessageStore, use: useMessageStore }
]
</script>
<template>
<StoreProvider :stores="stores">
<Main />
</StoreProvider>
</template>When the child components get the store from useStore(MessageStore), they will get the same store returned by useMessageStore() that was created before, not create new messageStore.
Component.vue
<script setup>
import { useStore } from 'pinia-di';
import { MessageStore } from '@/stores/messageStore';
// messageStore === useMessageStore(): true
const messageStore = useStore(MessageStore);
</script>stores/userStore.ts
import { defineStore } from 'pinia';
import { useStoreId } from 'pinia-di';
export const UserStore = ({ getStore, useStoreId }: InjectionContext) => {
return defineStore(useStoreId('user'), () => {
const state = reactive({});
const test = () => {
// get another store that the parent component or self provided
const appStore = getStore(AppStore);
console.log(appStore.xxx);
};
return {
state,
test
}
});
}stores/appStore.ts
import { defineStore } from 'pinia';
export const AppStore = ({ onUnmounted, useStoreId }: InjectionContext) => {
// define store, useStoreId('main') generate the unique id per `Store Instance`
return defineStore(useStoreId('main'), () => {
const state = reactive({});
const dispose = async () => {
// console.log('dispose')
};
onUnmounted(dispose);
return {
state
};
});
}If the same store creator is provided by more than one parent, the useStore will get the nearest one.
ParentA.Vue
<template>
<ParentB/>
</template>
<script setup>
import { StoreProvider } from 'pinia-di';
import { TestStore } from '@/stores/testStore';
const stores = [TestStore];
</script>
<template>
<StoreProvider :stores="stores">
<ParentB />
</StoreProvider>
</template>ParentB.Vue
<template>
<Child/>
</template>
<script setup>
import { provideStores } from 'pinia-di';
import { TestStore } from '@/stores/testStore';
const stores = [TestStore];
</script>
<template>
<StoreProvider :stores="stores">
<Child />
</StoreProvider>
</template>Child.Vue
<script setup>
import { useStore } from 'pinia-di';
import { TestStore } from '@/stores/testStore';
// will get the store provided by ParentB
const testStore = useStore(TestStore);
</script>pinia-di will call store.$dispose() when the injected component is unmounted.
If do not want this, you can use disposeOnUnmounted to disable it.
<script setup>
import { provideStores } from 'pinia-di';
import { TestStore } from '@/stores/testStore';
const stores = [
{ creator: TestStore, disposeOnUnmounted: false }
];
</script>