Skip to content

Composition API reference #97

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

Merged
merged 29 commits into from
May 28, 2020
Merged

Conversation

NataliaTepluhina
Copy link
Member

This is mostly a copy of https://vue-composition-api-rfc.netlify.app/api.html with few subtle changes. I believe the reference there is sufficient enough (maybe sometimes too detailed). Would be happy to hear your thoughts!

Copy link
Member

@znck znck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of information in this PR, I feel I might have missed something so I would do another pass on it. (this week sometime)

}
```

`attrs` and `slots` are proxies to the corresponding values on the internal component instance. This ensures they always expose the latest values even after updates so that we can destructure them without worrying accessing a stale reference:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's little unclear. We should unambiguously state that context can be destructured but attrs and slots cannot be. Destructuring e.g. setup(props, { attrs: { name } }) would break attrs reactivity.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I've split this statement onto 2 parts, so hopefully, it's clearer now:

Unlike props, context argument can be destructured safely so attrs and slots would always expose the latest values even after updates:

const MyComponent = {
  setup(props, { attrs }) {
    // a function that may get called at a later stage
    function onClick() {
      console.log(attrs.foo) // guaranteed to be the latest reference
    }
  }
}

However, attrs and slots themselves cannot be destructured without losing reactivity:

const MyComponent = {
  setup(props, { attrs: { foo } }) {
    function onClick() {
      console.log(foo) // won't be the latest reference as we lost `attrs` reactivity with destructuring
    }
  }
}


- **Usage of `this`**

**`this` is not available inside `setup()`.** Since `setup()` is called before other component options are resolved, `this` inside `setup()` (if made available) will behave quite differently from `this` in other options. Making it available will likely cause confusions when using `setup()` along other 2.x options. Another reason for avoiding `this` in `setup()` is a very common pitfall for beginners:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel we can drop the if made available as this hypothetical situation is not providing any value. And the previous statement seems enough reasoning for the unavailability of this in setup().

Copy link
Member

@phanan phanan May 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds pretty weird to me that we're saying "this is not available" at first and right after that "[this] will behave quite differently," which implies that it's still available. AFAIK, this is always available. Whether it's the Vue instance or the parent context (global/window) is another story.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rephrased the explanation so now we rather say this won't be a reference to Vue instance:

  • Usage of this

    Inside setup(), this won't be a reference to Vue instance Since setup() is called before other component options are resolved, this inside setup() will behave quite differently from this in other options. This might cause confusions when using setup() along other Options API. Another reason for avoiding this in setup() is a very common pitfall for beginners:

    setup() {
      const that = this
      function onClick() {
          console.log(this !== that) // not the `this` you'd expect!
      }
    }


- **Access in Templates**

When a ref is returned as a property on the render context (the object returned from `setup()`) and accessed in the template, it automatically unwraps to the inner value. There is no need to append `.value` in the template:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stands correct for inline event handlers, which can be confusing, given that the same event handler in setup() would have to use .value. I feel we should add an example for this scenario.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added this explanation:

However, if we decide to change the inline event handler on button click to the component method declared in setup, we need to remember that ref is not unwrapped there:

<template>
  <div>
    <span>{{ count }}</span>
    <button @click="increment">Increment count</button>
  </div>
</template>

<script>
  import { ref } from 'vue'
  export default {
    setup() {
      const count = ref(0)
      function increment() {
        count.value++ // ref is not unwrapped outside the template, so we need to use .value here
      }
      return {
        count,
        increment
      }
    }
  }
</script>


```html
<template>
<div ref="root"></div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of context but :ref="root" behaves differently and it's an easy pitfall to use v-bind with ref as it feels so natural. I feel we should be explicit about it and convey that ref="root" is equivalent to :ref="el => root = el".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a warning there:

:::warning
Please note that ref will behave differently when used with v-bind directive. ref="root" would be equivalent to :ref="el => root = el"
:::

However, I don't feel good about it as it adds an unnecessary cognitive load (at least for me it does) 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof yeah that is confusing... 😕

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that's template ref 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind :D 🤦

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I don't feel good about it as it adds an unnecessary cognitive load (at least for me it does) 😅

True! Should we have separate caveats or faq or common pitfalls section?

Copy link
Member

@znck znck May 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I feel this should be handled in runtime. This confusion is an unnecessary burden on users.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@znck I probably should remove it for now


Check if an object is a reactive proxy created by `reactive`.

It also returns `true` if the proxy is created by `readonly`, but is wrapping another proxy created by `reactive`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deserves an example.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an extended example here:

isReactive

Check if an object is a reactive proxy created by reactive.

import { reactive, isReactive } from 'vue'
export default {
  setup() {
    const state = reactive({
      name: 'John'
    })
    console.log(isReactive(state)) // -> true
  }
}

It also returns true if the proxy is created by readonly, but is wrapping another proxy created by reactive.

import { reactive, isReactive, readonly } from 'vue'
export default {
  setup() {
    const state = reactive({
      name: 'John'
    })
    // readonly proxy created from plain object
    const plain = readonly({
      name: 'Mary'
    })
    console.log(isReactive(plain)) // -> false

    // readonly proxy created from reactive proxy
    const stateCopy = readonly(state)
    console.log(isReactive(stateCopy)) // -> true
  }
}

NataliaTepluhina and others added 2 commits May 18, 2020 19:21
Co-authored-by: Rahul Kadyan <hi@znck.me>
Co-authored-by: Rahul Kadyan <hi@znck.me>
Copy link
Member

@phanan phanan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏👏👏


- **Usage of `this`**

**`this` is not available inside `setup()`.** Since `setup()` is called before other component options are resolved, `this` inside `setup()` (if made available) will behave quite differently from `this` in other options. Making it available will likely cause confusions when using `setup()` along other 2.x options. Another reason for avoiding `this` in `setup()` is a very common pitfall for beginners:
Copy link
Member

@phanan phanan May 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds pretty weird to me that we're saying "this is not available" at first and right after that "[this] will behave quite differently," which implies that it's still available. AFAIK, this is always available. Whether it's the Vue instance or the parent context (global/window) is another story.

Comment on lines 23 to 36
import { ref, reactive } from 'vue'

export default {
setup() {
const count = ref(0)
const object = reactive({ foo: 'bar' })

// expose to template
return {
count,
object
}
}
}
Copy link
Member

@phanan phanan May 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm quite sure we don't indent the first level of code inside <script> and <style> 😊

Copy link
Member

@Jinjiang Jinjiang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @NataliaTepluhina I'm thinking moving Reactivity-related APIs (Reactivity APIs + Reactivity Utilities + Advanced Reactivity APIs) as a independent page, before Composition API page. Because it's not only useful to Composition API, but also to Options API, even out of Vue Components.

For example:

export default {
  data() {
    return {
      somethingReadonly: readonly(foo),
      somethingWontChange: toRaw(bar)
    }
  }
}

or:

// shared.js
export const someGlobalData = reactive(baz)
<script>
import { someGlobalData } from './shared'
export default {
  setup() {
    return {
      x: someGlobalData
    }
  }
}
</script>

For others like provide, inject and lifecycle hooks, they must be used inside setup. So I think is make sense to leave them here.

WDYT 😅

Thanks.

`setup` can also return a render function, which can directly make use of reactive state declared in the same scope:

```js
import { h, ref, reactive } from 'vue'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we introduce h somewhere in our API Reference? It seems not appeared so far.

Copy link
Member Author

@NataliaTepluhina NataliaTepluhina May 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jinjiang the suggestion about Reactivity APIs makes perfect sense to me! Will separate it from the Composition API

As for h, I was planning on smth like Global Vue API to have h, defineComponent etc there. Not sure about naming though 🤔

@NataliaTepluhina NataliaTepluhina force-pushed the composition-api-reference branch from 9215661 to a4b6494 Compare May 23, 2020 14:35
@NataliaTepluhina
Copy link
Member Author

@znck @phanan @Jinjiang thank you all for all the awesome fixes and suggestions! I've changed the PR accordingly, please take a second look whenever you have a chance 🙏

@NataliaTepluhina NataliaTepluhina requested a review from pikax May 23, 2020 15:05
@NataliaTepluhina
Copy link
Member Author

@pikax sorry, I failed to add you to reviewers list on the 1st review 🤦‍♀️

Would be happy if you could take a look!

@Jinjiang
Copy link
Member

@NataliaTepluhina for Reactivity APIs I have some further ideas. When I'm trying to find the usage of these APIs for a special purpose like read-only data, I need to know readonly, isReadonly, and shallowReadonly, markRaw at the same time. But they are in 3 different sub pages. It feels like a little bit inconvenient.

I think for guidelines it's better to separate things into basic and advanced parts. But for references, maybe it's better to separate them into groups for different cases or purposes, like:

  • All proxy usage: reactive, readonly, isProxy, isReactive, isReadonly, shallowReactive, shallowReadonly, markRaw, toRaw.
  • All ref usage: ref, isRef, toRef, toRefs, customRef, shallowRef, unref.
  • Computed and watch: computed, watch, watchEffect.

Does that make more sense? ... or just don't group them and assemble them into one page together?

Thanks 😄

@NataliaTepluhina
Copy link
Member Author

@Jinjiang What if we place them on the same page (Reactivity API) but group them with subsections you mentioned?

@Jinjiang
Copy link
Member

I personally think that’s great 🙂 @NataliaTepluhina

@NataliaTepluhina
Copy link
Member Author

NataliaTepluhina commented May 24, 2020

@Jinjiang I still ended up dividing it onto 3 pages just for the sake of the effective sidebar usage: if we place them to the same page, some sections will go to h4 and they won't be shown on the sidebar. To improve UX with better navigation, I've done this splitting and added lots of cross-links between sections. Could you please take another look?

Also, a huge thanks for these suggestions! It makes lots of sense in terms of structuring the API properly 👍

Copy link
Member

@bencodezen bencodezen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job with all the edits and updates @NataliaTepluhina!

This is a very dense section and I have an instinct to come up with a similar approach to the Essentials of Vue.js so we can gradually build people up for a conceptually heavy topic, but it will take some time for me to come up with the proposal.

Nice work!


```html
<template>
<div ref="root"></div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof yeah that is confusing... 😕

@Jinjiang
Copy link
Member

@NataliaTepluhina that looks much better to me :-) Just one more little struggled idea: how about change the title "Proxy-related APIs" to "Basic" directly? Because:

  1. People may not know the Proxy things behind these APIs before them deeply look into it.
  2. This part is more like inherited + extended from the "traditional" Vue.observable() way in Vue 2.x.

WDYT? :-)

@NataliaTepluhina
Copy link
Member Author

@Jinjiang done! 😅


> This section uses [single-file component](TODO: SFC) syntax for code examples

## `setup`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and I am still not sure where we should keep it (because it's a component option but it's also a special option

})
```

An async function implicitly returns a Promise, but the cleanup function needs to be registered immediately before the Promise resolves. In addition, Vue relies on the returned Promise to automatically handle potential errors in the Promise chain.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I'm being blind, but I can't understand this paragraph with the example context, shouldn't we use onInvalidate on the async effect?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pikax would something like this work?

const data = ref(null)
watchEffect(async onInvalidate => {
  data.value = await fetchData(props.id)
  onInvalidate(() => {...})
})

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that case you would only register onInvalidate after the fetch is done, is useful if you do something like:

export default defineComponent({
  props: {
    id: Number,
  },

  setup(props) {
    const data = ref(null);
    watchEffect(async (onInvalidate) => {
      let controller = new AbortController();
      onInvalidate(() => {
        if (controller) {
          controller.abort();
          console.log("aborted");
        }
      });

      const response = await fetch(`https://swapi.dev/api/people/${props.id}`, {
        signal: controller.signal,

        mode: "cors",
      });

      data.value = await response.json();

      // clear the controller
      controller = null;
    });

    return {
      data,
    };
  },
});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing again, and on my first pass I misunderstood what this example is all about, probably add a gotcha and be more explicit of using onInvalidate after the promise is resolved might cause unexpected issues. Or add the correct way of doing it, ie register the onInvalidate before the promise resolve.

NOTE: Sorry for my misunderstanding

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pikax no need to be sorry! It wasn't clear indeed.

I fixed an example and added a comment

@Jinjiang
Copy link
Member

@NataliaTepluhina looks great. no more from my side. :-)

@NataliaTepluhina
Copy link
Member Author

@pikax thank you for the detailed review! I've addressed your comments, would you mind taking a look again? 😅

@NataliaTepluhina NataliaTepluhina requested a review from pikax May 26, 2020 17:59
Copy link
Member

@znck znck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎊 👏 looks great!!

@NataliaTepluhina
Copy link
Member Author

Thanks everyone for the review! I am merging this beast so if we have any fixes, let's handle them in the follow-up PRs

@NataliaTepluhina NataliaTepluhina merged commit 189453f into master May 28, 2020
@NataliaTepluhina NataliaTepluhina deleted the composition-api-reference branch July 15, 2020 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants