vuejs / pinia

🍍 Intuitive, type safe, light and flexible Store for Vue using the composition api with DevTools support
https://pinia.vuejs.org
MIT License
13.19k stars 1.06k forks source link

Allow changing pinia root store for component scope #870

Open bodograumann opened 2 years ago

bodograumann commented 2 years ago

What problem is this solving

When documenting a component with storybook and its docs addon, multiple stories are rendered in one MDX document. That means they are technically part of the same Vue application. Semantically however, each story represents a very specific state of the component. Usually these components would not access any global state (and thus pinia) at all, but sometimes it is necessary. That means each of the story components needs its own, independant “global” state.

Proposed solution

Pinia should allow to change the root store Pinia instance which is injected into a component and its descendents. Technically this could be done simply by exposing the piniaSymbol and than I could use provide(piniaSymbol, createPinia()).

On the other hand it might be more prudent to keep the symbol private and expose a convenience function providePinia(pinia) { provide(piniaSymbol, pinia ?? createPinia()) }.

Describe alternatives you've considered

It is also possible to patch alias fields into the storybook webpack config and mock the store definition module which provides the useMyStore function for each store individually. Then each story could set a currentMock property in those modules. This would be much more complex however, harder to understand and possibly fragile.

posva commented 2 years ago

Probably a providePinia(pinia) (the argument should be mandatory) is the best way so the current app and other properties can be set (like in pinia.install). This should be an easy contribution!

This is like nested pinias

cexoso commented 2 years ago

I notice the code below const piniaSymbol = ((process.env.NODE_ENV !== 'production') ? Symbol('pinia') : /* istanbul ignore next */ Symbol()); you can just change Symbol('pinia') to Symbol.for('pinia'). Symbol.for('pinia') allow developer overwrite the Provide by using Vue's provider with the Symbol.for('pinia') (because Symbol.for('pinia') equal Symbol.for('pinia'))

or export const piniaSymbol

ropez commented 2 years ago

I'm also looking for something like this, but for a different use-case. I want to explain here, and maybe get some opinions about it.

TL;DR I don't want global state across all pages in the application, but "global state" on each page.

We have a quite large application, with many pages (around 200), all of which are kind of "single page applications" themselves, with data, filters, side bars, popups etc. There is a big navigation menu, and when the user clicks on a different page in the menu, the expected behavior is that the destination page opens with a clean state. If they navigate back to the previous page, it's not expected to maintain the state that it had before (unless it's explicitly set up to do that via some persistence mechanism).

Basically, it's supposed to work as if each menu click re-loads the page. However, we're using vue-router to navigate between the pages without re-loading, because it gives a much more responsive experience.

We're not currently using any state management library, but just a combination of component state, and ad-hoc use of provide/inject on the different pages. I think it would be beneficial to have something like pinia, if we could make each page create a completely independent store, which is disposed when navigating away from the page.

We could perhaps achieve this by calling $reset when opening a page, but it doesn't feel natural. We would easily forget to do it, and create unwanted behavior that would be missed by QA. Also, it seems wasteful to have a big store holding state from all previously visited pages, that we don't need.

I think something like providePinia, would be perfect for our use-case. Alternatively, if the Pinia instance provided a global reset function, that basically destroyed all registered stores, and we could hook that into the router, perhaps that would work.

johannes-z commented 2 years ago

A similar use case is having multiple, complex components in a single page (or nested components -> same app), whereas each component should handle its own pinia instance/store.

invokermain commented 2 years ago

+1 on this! I think this is a great feature. Simplifying complex nested components with a store is great for keeping things clean. But having the store be a global/singleton means that component is then a bit less reusable/isolated.

Being able to 'scope' your stores to components means the components then are completely self contained/reusable and benefit from the cleaner architecture that using something like pinia provides 😄

Then you could have something neat like (complete pseudocode & probably not the best way to achieve).

<script setup>
...
const props = {
    myId: number
}

const piniaInstance = createPinia()
const componentStore = useComponentStore(piniaInstance)

componentStore.load(myId)
</script>

<template>
    <sub-one :store="componentStore" />
    <sub-two :store="componentStore" />
</template>

Because currently you would have something like this in a sub component:

<script setup>
...
const componentStore = useComponentStore()
</script>

<template>
    ... use the componentStore
</template>

But now the sub component isn't self-documenting or that reusable... it assumes that the store has been loaded with the right data. How do we know what component is in charge of loading the store etc? You can't show two of the component with different data etc.

ccqgithub commented 1 year ago

When multiple components want to use same store, or a component that with a store will render multiple times, the reusable of store is important.

Now, i can't find a solution in pinia, so i write a tool to reuse store, the repo is here: pinia-di.

its-lee commented 1 year ago

I'm also looking for something like this, but for a different use-case. I want to explain here, and maybe get some opinions about it.

TL;DR I don't want global state across all pages in the application, but "global state" on each page.

We have a quite large application, with many pages (around 200), all of which are kind of "single page applications" themselves, with data, filters, side bars, popups etc. There is a big navigation menu, and when the user clicks on a different page in the menu, the expected behavior is that the destination page opens with a clean state. If they navigate back to the previous page, it's not expected to maintain the state that it had before (unless it's explicitly set up to do that via some persistence mechanism).

Basically, it's supposed to work as if each menu click re-loads the page. However, we're using vue-router to navigate between the pages without re-loading, because it gives a much more responsive experience.

We're not currently using any state management library, but just a combination of component state, and ad-hoc use of provide/inject on the different pages. I think it would be beneficial to have something like pinia, if we could make each page create a completely independent store, which is disposed when navigating away from the page.

We could perhaps achieve this by calling $reset when opening a page, but it doesn't feel natural. We would easily forget to do it, and create unwanted behavior that would be missed by QA. Also, it seems wasteful to have a big store holding state from all previously visited pages, that we don't need.

I think something like providePinia, would be perfect for our use-case. Alternatively, if the Pinia instance provided a global reset function, that basically destroyed all registered stores, and we could hook that into the router, perhaps that would work.


On this, rather than a pinia-side change; you could have an enforced version of the $reset workflow you're suggesting so that it couldn't be forgotten (I'm aware you're saying this feels unnatural, which is fair):

This automates the process you've described - although relies on you being aware that if you (for some reason) access a different page's store - that any changes made to that store won't be $reset but will be reset by accessing that different page.

IonianPlayboy commented 3 weeks ago

I just found out this issue while investigating why the docs generated by Storybook was not playing nicely with some components, and I'm glad there was something to help me understand what was going on.

However, while this issue presents its change as a nice to have, I would argue that it's solving a real problem relevant to end users right now. Currently the Storybook/Pinia integration can create bugs that are hard to understand without the context highlighted here.

Most examples and docs I could find online (like this official tutorial) recommands this way in order to register a Pinia instance for Storybook stories :

import { setup } from '@storybook/vue3';

import { createPinia } from 'pinia';

//👇 Registers a global Pinia instance inside Storybook to be consumed by existing stories
setup((app) => {
   app.use(createPinia());
});

const preview = {
  /* ... */
};

export default preview;

This approach will register a different Pinia instance for each story, and works fine in most cases. I found out the hard way that it creates a bug when some conditions are met:

If that's the case, then the outside logic that updates the store state won't affect the correct active Pinia instance, it will be dependant on the last instance that was set as active.

Here is a repro with the bug :

This is a simplified repro of my use case : I'm wrapping in a component some third-party code that creates an instance with a distinct id. When this instance emits a specific event, I have a callback that updates the active id inside a store in order to display a modal with the relevant data. This is all happening outside of Vue components.

That's why I would love to see this issue being worked on. In the meantime, I think it would also be nice to add some documentation about the current behavior, but I'm not sure if that would be more appropriate here or on the Storybook repo.

If someone else encounters the same problem, the best way to solve it is to create a single Pinia instance to be shared between all your stories :

import { setup } from '@storybook/vue3';

import { createPinia } from 'pinia';

//👇 Create a single Pinia instance outside of `setup()` to share it between all stories
const pinia = createPinia();

setup((app) => {
  app.use(pinia);
});

const preview = {
  /* ... */
};

export default preview;
posva commented 3 weeks ago

@IonianPlayboy The issue you are facing is unrelated to this issue. It's because you are calling the useStore() where it shouldn't. See https://pinia.vuejs.org/core-concepts/outside-component-usage.html#Using-a-store-outside-of-a-component for more information.