pinia icon indicating copy to clipboard operation
pinia copied to clipboard

Could a feature (autoToRef) be implemented to automatically return (toRefs) when destructuring the state?

Open danielpaz98 opened this issue 2 years ago • 15 comments

What problem is this solving

I actually came up with this because it is a bit annoying to add storeToRefs to each store if you want to destructuring

Proposed solution

I'm very new to this, it's the first time I do it, and while I was thinking how it could be implemented I found a repository where this function is better explained, I don't know if I can put the link?

danielpaz98 avatar Oct 12 '21 22:10 danielpaz98

One or multiple example would help understand what you want by auto refs and also if it's technically possible or not. Also note that you don't need to use storeToRefs(), you can use the store directly as a whole

posva avatar Oct 12 '21 23:10 posva

what I meant was that it's a bit annoying to import all the time storeToRefs, what I mean would be this:

import { useMainStore } from "~/stores/mainStore";
import { storeToRefs } from "pinia";

const mainStore = useMainStore();
const { foo, bar } = storeToRefs(mainStore);

I think if there was an option (autoToRef) where I could decide if I want the storeToRefs to be done automatically when the store is destructuring that would be great.

import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

const pinia = createPinia({
  autoToRef: true,
});

const app = createApp(App);
app.use(pinia);
app.mount("#app");

then I would only have to do:

import { useMainStore } from "~/stores/mainStore";
const { foo, bar } = useMainStore();

maybe this package will help you to better understand the idea, I repeat I am a newbie, it is just an idea, but not a bad one. https://www.npmjs.com/package/vue-banque

danielpaz98 avatar Oct 12 '21 23:10 danielpaz98

Maybe this is possible with a vite plugin and I'm open to adding it to the main repo but I don't have the bandwidth to do it.

Another solution is to create your own defineStore() wrapper that calls storeToRefs() on the resulting store when passing an argument (to still enable extracting actions). Or even better a custom version that allows destructuring anything.

I think that last version is the most interesting one

posva avatar Oct 13 '21 06:10 posva

Could someone exemplify how would be this custom function that would allow to destructure anything?

alijuniorbr avatar Feb 26 '22 02:02 alijuniorbr

this is my solution to this:

store example:

import { defineStore } from "pinia";

const store = defineStore("global", {
  state: () => ({
    previousRoute: "",
  }),
});

export default store;

@helpers/pinia-auto-refs.js

import { storeToRefs } from "pinia";

const storeFiles = import.meta.globEager("./../store/*.js");

let storeExports = {};

for (const fileName in storeFiles) {
  const { default: storeObject } = storeFiles[fileName];

  storeExports[storeObject.$id] = storeObject;
}

export function useStore(storeName = "global") {
  if (!Object.keys(storeExports).includes(storeName)) {
    throw "Unknown pinia store name: " + storeName;
  }

  const store = storeExports[storeName]();
  const storeRefs = storeToRefs(store);

  return { ...store, ...storeRefs };
}

and unplugin autoimports config:

{
  dts: "auto-imports.d.ts",
  include: [
    /\.vue$/,
    /\.vue\?vue/, // .vue
  ],
  imports: [
    // presets
    "vue",
    "vue-router",
    {
      "@helpers/pinia-auto-refs.js": ["useStore"],
    },
  ],
}

add unplugin to vite config plugins:

import unpluginAutoImport from "unplugin-auto-import/vite";

//..

plugins: [
      // ...
      unpluginAutoImport(unpluginAutoImportConfig),
     // ...
]

example usage:

const { statePropExample, getterExample, actionExample } = useStore("storeName");
// leave useStore() to access to store named global -> useStore("globall")

pros:

  • easy to use

cons:

  • no intelisense

vieruuuu avatar Mar 20 '22 10:03 vieruuuu

I have an alternative for my useStore function that has intelisense support

// defineRefStore.js
import { defineStore, storeToRefs } from "pinia";

export function defineRefStore(id, storeSetup, options) {
  const piniaStore = defineStore(id, storeSetup, options);

  return () => {
    const usedPiniaStore = piniaStore();

    const storeRefs = storeToRefs(usedPiniaStore);

    return { ...usedPiniaStore, ...storeRefs };
  };
}

also a d.ts file for intelisense

// defineRefStore.d.ts
import {
  DefineSetupStoreOptions,
  _ExtractStateFromSetupStore,
  _ExtractGettersFromSetupStore,
  _ExtractActionsFromSetupStore,
  Store,
  PiniaCustomStateProperties,
} from "pinia";

import { ToRefs } from "vue-demi";

export declare function defineRefStore<Id extends string, SS>(
  id: Id,
  storeSetup: () => SS,
  options?: DefineSetupStoreOptions<
    Id,
    _ExtractStateFromSetupStore<SS>,
    _ExtractGettersFromSetupStore<SS>,
    _ExtractActionsFromSetupStore<SS>
  >
): () => Store<
  Id,
  _ExtractStateFromSetupStore<null>,
  _ExtractGettersFromSetupStore<null>,
  _ExtractActionsFromSetupStore<SS>
> &
  ToRefs<
    _ExtractStateFromSetupStore<SS> &
      _ExtractGettersFromSetupStore<SS> &
      PiniaCustomStateProperties<_ExtractStateFromSetupStore<SS>>
  >;

now when defining stores just use defineRefStore instead of defineStore from pinia

example:

import { defineRefStore } from "./defineRefStore.js";

import { ref, computed } from "vue";

export const useCartStore = defineRefStore("cart", () => {
  const products = ref([]);

  return { products };
});

NOTE I WROTE THIS FUNCTION ONLY FOR SETUP STORES

vieruuuu avatar Apr 20 '22 09:04 vieruuuu

Hi,I implemented ts version based on your idea.And publish it as a Vite plugin.I used it in one of my projects and it worked fine and looked fine.It's easy to use and has intelisense.

image

plugin: pinia-auto-refs playground

Allen-1998 avatar May 09 '22 20:05 Allen-1998

Couldn't this be done as a helper of sorts?

const warehouseStore = useWarehouseState();
const { setWarehouseState } = warehouseStore;
const { warehouse, resourceVolume } = storeToRefs(warehouseStore);

As an example, this is quite annoying.

Couldn't this be wrapped up into a helper function that returns a combined object to the effect of:

// Contrived example, but internally the helper would do something similar?
function someHelper() {
    const warehouseStore = useWarehouseState();
    const { setWarehouseState } = warehouseStore;
    const { warehouse, resourceVolume } = storeToRefs(warehouseStore);

    return {
        setWarehouseState,
        warehouse,
        resourceVolume
    };
}

Which we could then use as:

const { warehouse, resourceVolume, setWarehouseState } = someHelper(useWarehouseState());

Or does this just end up breaking things?

douglasg14b avatar Aug 07 '22 01:08 douglasg14b

Found this annoying as well. There's no point in unwrapping all refs in a setup store, since everything returned from it will already have been a ref/reactive/computed or marked raw.

I ended up writing this version of defineStore which includes a modified version of storeToRefs in it.

  • Keeps refs/computed/markedRaw as is
  • Additionally, keeps reactive as is (unlike storeToRefs which wraps them)
  • Removes extra methods from the store (allows combining stores)
  • Has working Go to definition (tested in webstorm)
  • Doesn't break dev tools (as far as I can tell)

https://gist.github.com/unshame/1deb26be955bc5d1b661bb87cf9efd69


const useInnerStore = defineStore('innerStore', () => {
    return {
        innerRef: ref('baz'),
    };
});

const useStore = defineStore('store', () => {
    return {
        reactiveProp: reactive({ field: 'foo' }),
        refProp: ref('bar'),
        rawProp: markRaw({ key: "I'm raw" }),

        ...useInnerStore(), // combining stores
    };
});

const { reactiveProp, refProp, rawProp, innerRef } = useStore();

console.log(reactiveProp.field); // foo
console.log(refProp.value); // bar
console.log(isRef(rawProp) || isReactive(rawProp)); // false
console.log(innerRef.value); // baz

The only downside is that some internal types had to be imported and rewritten to remove Unwrap from them.

unshame avatar Feb 15 '23 16:02 unshame

@vieruuuu Love your work!

I think your thin wrapper over defineStore achieves the goal of removing storeToRefs() boilerplate code that this topic is about. Nonetheless I wanted to mention that if not using destructuring, there are some differences that people who are used to pinia's pattern need to be cautious, for example, without defineRefStore:

const store = useStore();
store.prop1 = v1;
store.prop2++;

with defineRefStore:

const store = useStore();
store.prop1.value = v1;
store.prop2.value++;

hermit99 avatar Jul 04 '23 07:07 hermit99

Proposal for storeToRefs alternative!

--> store

const show = getter(false); // same as a ref
const countObj = reactiveGetter({ count: 0 }); // same as reactive
const user = ref(false);  // not exported
const getUser = () => console.log('user');

--> component

const { show, countObj, getUser } = useUserStore(); 

Essentially this would mean that ref and reactive would not be auto exported, getter and reactiveGetter (or whatever the term we are using here) on the other hand will be. Functions will be always exported. This way we would reduce the amount of work wherever a store is required.

To get a ref or reactive, this would mean storeToRefs would be still required.

Subwaytime avatar Aug 01 '23 14:08 Subwaytime

It would be better if it didn't get converted to reactive in the first place, instead of turning it into reactive and then back into refs.

coolCucumber-cat avatar Nov 23 '23 10:11 coolCucumber-cat

See my issue: https://github.com/vuejs/pinia/issues/2504#issue-2007826470

coolCucumber-cat avatar Nov 23 '23 10:11 coolCucumber-cat

Edit: this doesn't always work, I added some more code and it broke reactivity again, no clue what's up with this.

I don't know if this solves things for you guys, but I ran into this while coding an example application for an assignment:

here's my store:

import {ref} from 'vue'
import {defineStore} from 'pinia'

export const useTodoStore = defineStore('counter', () => {
    const todos = ref<{text:string, completed:boolean}[]>([])
    function addTodo(todo: {text:string, completed:boolean}) {
        todos.value.push(todo)
    }
    return {todos, addTodo}
})

here's how I destructure it

const {todos, addTodo} = useTodoStore()

this destructures the store with the todos ref still functioning as intended (it can be updated and my template adjusts accordingly) which would not be possible if I had done:

const store = useTodoStore()
const {todos, addTodo} = store

might be intended, might not be, either way, seems to be a good fit for this issue and I didn't see anyone bring it up.

CmD0 avatar Dec 12 '23 18:12 CmD0

Using storeToRefs to create a new defineStore

Here's my version of defineStore, I've been using this for some time now, this version of defineStore won't have any noticeable runtime impact. As a role of thumb, I don't use reactive, and this function converts every reactive variable in store to ref. File store.ts

import { type StoreGeneric, defineStore, storeToRefs } from "pinia"
import type { ToRefs } from "vue"

// Extrancted from "utility-types" package
type NonUndefined<A> = A extends undefined ? never : A

type FunctionKeys<T extends object> = {
	[K in keyof T]-?: NonUndefined<T[K]> extends Function ? K : never;
}[keyof T]

type NonFunctionKeys<T extends object> = {
	[K in keyof T]-?: NonUndefined<T[K]> extends Function ? never : K;
}[keyof T]

/**
@description Creates a useStore function that retrieves the store instance
@param id — id of the store (must be unique)
@param storeSetup — function that defines the store
 */
export function defineComposable<Id extends string, SS>(id: Id, storeSetup: () => SS) {
	const piniaStore = defineStore(id, storeSetup)

	return () => {
		const store = piniaStore()
		const storeRefs = storeToRefs(store as StoreGeneric)

    // Pick only function properties from source store.
    // And append refs to final output.
    type StoreFunctions = Pick<typeof store, FunctionKeys<typeof store>> & NonNullable<unknown>
		type NonFunctions = ToRefs<Pick<typeof store, NonFunctionKeys<typeof store>>>

		type OmittedValues = Omit<NonFunctions, "_customProperties" | "$state" | "$id">
		type OmittedFunctions = Omit<StoreFunctions, "$onAction" | "$patch" | "$reset" | "$subscribe">

		return { ...store as OmittedFunctions, ...storeRefs as OmittedValues }
	}
}

And here's the usage example.

File counter.ts

import { defineComposable } from "./store.ts"

export const useCounter = defineComposable("counter", () => {
	const count = ref(0)
	const name = ref("Eduardo")
	const doubleCount = computed(() => count.value * 2)
	function increment() {
		count.value++
	}

	return { count, name, doubleCount, increment }
})

File app.vue

<script setup lang="ts">
import { useCounter } from "./counter.ts"

const counter = useCounter()
counter.count.value = 10
</script>

<template>
{{ counter.count.value }}
</template>

If you want to keep the reactive as is, use the gist provided by @unshame

Saeid-Za avatar Jan 04 '24 14:01 Saeid-Za