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-di
flowchart 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 tree
is like thecomponent tree
, each component get the store from the nearest parentInjector
.StoreProvider
: A component that usesInjector
to provide stores.Store Use
: The return type of defineStore.Store
: The return type of aStore Use
function like useStore();Store Creator
: A function that returns aStore Use
.InjectionContext
: The parameter that theStore Creator
will 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>