vueuse / vueuse

Collection of essential Vue Composition Utilities for Vue 2 and 3
https://vueuse.org
MIT License
19.92k stars 2.52k forks source link

feat: `unrefy` #790

Closed shorwood closed 2 years ago

shorwood commented 3 years ago

Hello everyone,

While on a project, i had to dynamically call functions but the arguments I called them with were sometimes Refs. So my first thought was wrapping the function using the reactify utility; But then I remembered the wrapped function returns ComputedRef<T> and is evaluated on every changes of the arguments. And I want the function to only return T and only when I call it. I started looking for other utilities that might help and either did not found suitable one or did not search thoroughly enough.

This is where unrefy comes from. Below is it's documentation entry.

Pull Request : #789

Documentation

Convert a plain function into a function that unref it's aguments before every call. The converted function accepts refs and raw values as its arguments and returns the same value the unconverted function returns, with proper typing.

::: tip Make sure you're using the right tool for the job. Using reactify might be more pertinent in some cases where you want to evaluate the function on each changes of it's arguments. :::

Usage

import axios from 'axios'
import { ref } from 'vue-demi'
import { unrefy } from '.'

const url = ref('https://httpbin.org/post')
const data = ref({foo: 'bar'})
const post = (url, data) => axios.post(url, data)
const postUnrefied = unrefy(post)

post(url, data)           /* ❌ Will throw an error because the arguments are refs */
postUnrefied(url, data)   /* ✔️ Will Work because the arguments will be "unrefied" */

Related Functions

Source

import { unref } from 'vue-demi'
import { MaybeRef } from '../../shared/utils'

/** Unrefied method. */
type Unrefy<T> = T extends (...args: infer A) => infer R ? (...args: {
  [K in keyof A]: MaybeRef<A[K]>
}) => R : never

/**
 * Convert a plain function into a function that unref it's aguments before every call.
 * The converted function accepts refs as its arguments and returns the same value
 * the unconverted function returns, with proper typing.
 * @param fn - Source function
 */
export const unrefy = <T extends Function>(fn: T): Unrefy<T> => {
  return function(this: any, ...args: any[]) {
    return fn.apply(this, args.map(i => unref(i)))
  } as Unrefy<T>
}
antfu commented 3 years ago

That feels a bit weird to me. When you want to pass value to the function, I think you'd better explicitly add .value or the new ref sugar syntax. The case that reactify trying to approach is the build the connections, when you give a function reactive input, it should give reactive output. I understand the motivation for unrefy but I guess it might be a bit confusing to pass reactive data to a function that is supposed to be run once.

shorwood commented 3 years ago

I agree. Furthermore, the example provided in the documentation is unrealistic. I hardly see unrefy being used in a setup() context. The function kinda goes against the principle of Vue's reactivity.

However unrefy is meant to be sort of a shared function allowing cleaner integration with Vue-agnostic paradigms. I found it relatively useful when integrating external modules with Vue's Composition API, cleaning up composables, chaining wrappers while keeping types and implementing useful permissiveness.

tldr;

import { updateMakeModel, Make } from '~/src/services/makes'
import { unrefy } from '@vueuse/shared'
import { partial } from 'lodash-es'
const make = ref('toyota')

/** Without unrefy */
const updateModel = (model: MaybeRef<string>, data: MaybeRef<Make>, options: MaybeRef<UpdateMakeOptions>) => updateMakeModel(unref(make), unref(model), unref(data), unref(options))

/** With unrefy */
const updateModel = partial(unrefy(updateMakeModel), make)

Use case

Here is an analogue of the context I used it in : Let's say, for the sake of the argument, that we have a small external library acting as an SDK and providing the functions below. They are all Vue-agnostic and they don't know what a Ref is.

src/service/makes.ts

```ts /** Car manufacturer */ export interface Make { id: string, name: string, country: string, bankrupt: boolean models: string[] } /** Car model */ export interface Model { id: string, name: string, make: string year: number } /** Axios instance used to query */ const eventService = axios.create({baseURL: 'https://api.acme.com'}) /** Get a make from the API */ const getMake = async (make: string, params: GetMakeOptions): Make { return await eventService.get(`/makes/${make}`, { params }).then(res => res.data as Make) } /** Update a make in the API */ const updateMake = async (make: string, data: Make): Make { return await eventService.put(`/makes/${make}`, data).then(res => res.data as Make) } /** Remove a make from API */ const removeMake = async (make: string, data: Make) { return await eventService.delete(`/makes/${make}`).then(res => res.data) } /** Get model from a make from the API */ const getMakeModel = async (make: string, model: string, config: AxiosRequestConfig): Model { return await eventService.get(`/makes/${model}/${make}`, { params }).then(res => res.data as Model) } /** ... And few more utilities, you get the point. */ [...] ```

However, now we want to extend this SDK to make some composables out of it. We want a useMake composable that allows the developer to easily manipulate car manufacturers and their models from the setup() hook. We want the returned methods from useMake() to allow a small degree of permissiveness in the arguments (they can be reactive or not). And we want to keep the types intact.

Here is an example of how `useMake` could be used :

```ts setup(props) { /* ... */ //--- Init reactive variables. const make: Ref = ref({}) //--- Instantiate composable. const { get, update, remove } = useMake(props.make) onMounted(async () => make.value = await get()) //--- Here we can update the make's fields using a `Ref` and static values. const updateAsIs = () => update(make) const updateCountry = country => update({ country }) const updateAsBankrup = country => update({ bankrup: true }) //--- Return the `UseMake` object. return { make, updateCountry, updateAsIs, updateAsBankrup, remove } } ```

Example

Now for the important part. Below are two implementation of useMake, one with unrefy and one without.

Example using the `unrefy` function.

```ts import { MaybeRef } from '@vueuse/shared' import { unrefy } from '@vueuse/core' import { partial } from 'lodash-es' import { getMake, updateMake, removeMake, getMakeModel, addMakeModel, updateMakeModel, removeMakeModel } from '~/src/services/makes' /** Composable for manipulation makes from the API . */ export const useMake = ( make: MaybeRef, ) => { //--- Define instance utilities. const get = partial(unrefy(getMake), make) const update = partial(unrefy(updateMake), make) const remove = partial(unrefy(removeMake), make) const getModel = partial(unrefy(getMakeModel), make) const addModel = partial(unrefy(addMakeModel), make) const updateModel = partial(unrefy(updateMakeModel), make) const removeModel = partial(unrefy(removeMakeModel), make) //--- Return the `UseMake` object. return { get, update, remove, getModel, addModel, updateModel, removeModel } } ```

Example _not_ using the `unrefy` function.

```ts import { MaybeRef } from '@vueuse/shared' import { getMake, updateMake, removeMake, getMakeModel, addMakeModel, updateMakeModel, removeMakeModel } from '~/src/services/makes' /** Composable for manipulation makes from the API . */ export const useMake = ( make: MaybeRef, ) => { //--- Define instance methods. const get = (params: GetMakeOptions) => getMake(unref(make), unref(params)) const update = (data: MaybeRef) => updateMake(unref(make), unref(data)) const remove = () => removeMake(unref(make)) const getModel = (model: MaybeRef, params: GetMakeModelOptions) => getMakeModel(unref(make), unref(model), unref(params)) const addModel = (data: MaybeRef) => addMakeModel(unref(make), unref(data)) const updateModel = (model: MaybeRef, data: MaybeRef) => updateMakeModel(unref(make), unref(model), unref(data)) const removeModel = (model: MaybeRef) => removeMakeModel(unref(make), unref(model)) //--- Return the `UseMake` object. return { get, update, remove, getModel, addModel, updateModel, removeModel } } ```

Final notes

I know this bit of a weird use case; But you can find a example of how it could be used from one of the sketchy projects i'm working on.

antfu commented 3 years ago

I see. But maybe the ref sugar could be a better solution for this? https://github.com/vuejs/rfcs/discussions/369

shorwood commented 3 years ago

That would be one way to solve it. But would the RFC apply to legacy projects, the ones using @vue/composition-api or @nuxtjs/composition-api ? You would have to explicitly install the correct version of @vue/compiler-sfc, define refSugar: true or refTransform: true in the build config of these kind of projects.

antfu commented 3 years ago

https://github.com/antfu/unplugin-vue2-script-setup#configurations

shorwood commented 3 years ago

Didn't know this existed. Well I guess i'll check that out. Thanks for the feedback.

wheatjs commented 2 years ago

Closed via #789