Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
danielpaz98 opened this issue Oct 12, 2021 · 17 comments

Comments

@danielpaz98
Copy link

danielpaz98 commented Oct 12, 2021

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 danielpaz98 changed the title Could a feature (autoToRef) be implemented to automatically return the references on destructuring the state? Could a function (autoToRef) be implemented to automatically return (toRefs) when destructuring the state? Oct 12, 2021
@danielpaz98 danielpaz98 changed the title Could a function (autoToRef) be implemented to automatically return (toRefs) when destructuring the state? Could a feature (autoToRef) be implemented to automatically return (toRefs) when destructuring the state? Oct 12, 2021
@posva
Copy link
Member

posva commented Oct 12, 2021

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

@danielpaz98
Copy link
Author

danielpaz98 commented Oct 12, 2021

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

@posva
Copy link
Member

posva commented Oct 13, 2021

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

@alijuniorbr
Copy link

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

@vieruuuu
Copy link

vieruuuu commented Mar 20, 2022

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
Copy link

vieruuuu commented Apr 20, 2022

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

@Allen-1998
Copy link

Allen-1998 commented May 9, 2022

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

@douglasg14b
Copy link

douglasg14b commented Aug 7, 2022

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?

@posva posva added discussion feature request and removed ✨ enhancement New feature or request labels Aug 17, 2022
@unshame
Copy link

unshame commented Feb 15, 2023

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.

@hermit99
Copy link

hermit99 commented Jul 4, 2023

@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++;

@Subwaytime
Copy link

Subwaytime commented Aug 1, 2023

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.

@coolCucumber-cat
Copy link

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
Copy link

See my issue: #2504 (comment)

@CmD0
Copy link

CmD0 commented Dec 12, 2023

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.

@Saeid-Za
Copy link

Saeid-Za commented Jan 4, 2024

Using storeToRefs to create a new defineStore

Please check this comment for an updated answer.


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

@haykkh
Copy link

haykkh commented Oct 24, 2024

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

As of 2.2.3 this no longer works, the types are lost when consuming the store. If i understand correctly it's related to #2771

@Saeid-Za
Copy link

Hello There !
Here's my updated version after 2.2.3 update:
store.ts

import { defineStore, storeToRefs } from "pinia"

export function defineComposable<Id extends string, SS extends () => any>(
	id: Id,
	storeSetup: SS,
): () => ReturnType<SS> {
	const piniaStore = defineStore(id, storeSetup)

	return () => {
		const store = piniaStore()
		const storeRefs = storeToRefs(store) as ReturnType<SS>
		const composable = {} as ReturnType<SS>

		for (const key in store) {
			if (key in storeRefs) {
				composable[key] = storeRefs[key]
			}
			else {
				// @ts-expect-error Incorrect store typings
				composable[key] = store[key]
			}
		}

		return composable
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

No branches or pull requests