pinia-di: Use Pinia more flexibly!
🔥 🔥 Better way to reuse stores.
DI(dependency-injection) for pinia. work with vue@3.
flowchart TD
A{{"StoreProvider[AppStore, MessageStore]\nInjectorA"}} --> B["CompoentApp\nInjectorB\nconst appStore = useStore(AppStore)();\nconst messageStore = useStore(MessageStore)()"]
B --> C{{"StoreProvider[ChildStore]\nInjectorC"}}
B --> D{{"StoreProvider[ChildStore]\nInjectorD"}}
C --> E["ComponentChild\nInjectorE\nconst appStore = useStore(AppStore)();\nconst childStore = useStore(ChildStore)();"]
D --> F["ComponentChild\nInjectorF\nconst appStore = useStore(AppStore)();\nconst childStore = useStore(ChildStore)();"]
E --> G{{"StoreProvider[ChildStore]\nInjectorG"}}
G --> H["ComponentChild\nInjectorH\nconst appStore = useStore(AppStore)();\nconst childStore = useStore(ChildStore)();"]
Injector
: Inject and provide stores in the component tree to current component or child components.Store Tree
: Thestore tree
is like thecomponent tree
, each component get the store form the nearestInjector
.StoreProvider
: A component that useInjector
to provide stores.Store Use
: The return type of defineStore.Store
: The return type by call theStore Use
like useStore();Store Creator
: A function that return aStore Use
.InjectionContext
: The parameter that theStore Creator
will receive.
A Store Creator
is a creator function that return the defineStore
.
For example: the AppStore
is a Store Creator
, and the return of AppStore()
is Sotre 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 other store that have been provided by 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
use id
to identify one store, but our Store Creator
is reusable, so we need a 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 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);
// you can aslo remove the callback by
// 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 help 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 only use once, if changes after component mounted, the new stores prop will be ignored.
🔥 If you want to conditionally provide diffrent stores, you need to write diffrent 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.privide
for whole app.
pinia-di
provide 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([getProvideArgs], '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 use use Singleton Store
, you can't get InjectionContext
when then store create ***
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 proivide 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 store of useStore(MessageStore)
, they will get the useMessageStore()
that be created before, not to create new Store Use
.
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 other store that 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 for per `Store Instance`
return defineStore(useStoreId('main'), () => {
const state = reactive({});
const dispose = async () => {
// console.log('dispose')
};
onUnmounted(dispose);
return {
state
};
});
}
If same store creator
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 then inject component unmounted.
If do not want the, 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>