vuejs / rfcs

RFCs for substantial changes / feature additions to Vue core
4.87k stars 546 forks source link

Setup component with a generator function #180

Closed edimitchel closed 4 years ago

edimitchel commented 4 years ago

Rendered

It's my first RFC. :)

danielelkington commented 4 years ago

This would be a very nice feature that deals with the verbosity of the return statement. It also makes it possible to truly group everything by logical concern when you have distinct features that aren't sharing state:

// currently
function setup() {
  // Feature 1 (some stuff here)
  const feature1Stuff = reactive({
    ...
  })
  // more feature 1 logic

  // Feature 2
  const feature2Stuff = reactive({
    ...
  })
  // more feature 2 logic  

  // But then part of feature 1 is down here too!
  return { feature1Stuff, feature2Stuff }
}

// with this proposal
function* setup() {
  // Feature 1
  const feature1Stuff = reactive({
    ...
  })
  // more feature 1 logic
  yield feature1Stuff

  // Feature 2
  const feature2Stuff = reactive({
    ...
  })
  // more feature 2 logic
  yield feature2Stuff
}

In addition it will be really nice to get and expose values from a composition function in a single line, which is currently not possible.

  yield ({ x: mouseX, y: mouseY } = useMousePosition())

Even if some people may not like this approach (they may prefer to have a return statement – one place where they can see everything that gets exposed to the template), it would be a useful alternative to have.

jods4 commented 4 years ago

I feel like this would be better in a library.

Compare the RFC example:

// From RFC
const { id, setId } = useFeature()
yield { id, setId }
yield { name: ref('foo') }

// With:
const { id, setId } = useFeature()
return { 
  id, setId, 
  name: ref('foo') 
}

// or even:
return { ...useFeature(), name: ref('foo') }

The yield version is not particularly shorter or clearer.

I would suggest: if your setup is large and made of independent features, split them into shorter functions and write a short setup that merge them instead of a long one with parts separated by yields:

// Factor out independent features
setup() {
  return {
     ...feature1(),
     ...feature2(),
     ...feature3(),
  }
}

// Instead of 
*setup() {
  // long feature 1
  // ...
  yield { ... }

  // long feature 2
  // ...
  yield { ... }
}

If you prefer to use a generator you could easily do this with a library:

// Assume a defineYieldComponent function
// It simply replaces the setup on its argument with 
// setup = (props, ctx) => mergeYields(originalSetup(props, ctx))
// mergeYields is left as an exercise to the reader ;)
export default defineYieldComponent({
  *setup() {
    const { id, setId = useFeature() }
    yield { id, setId }
    yield { name: ref('foo') }
  }
})

This RFC would add a bit of complexity to core:

  1. Obviously: runtime must handle a new case where setup returns a generator.
  2. How are yielded values merged together and kept reactive? Do you expect a state = { ...toRefs(yield1), ...toRefs(yield2) }? It's less efficient and what if I yield a markRaw object (toRefs would warn)?
  3. There's risk that yields shadow each other with same keys. Since runtime is in charge of merging you need to specify what's gonna happen: last yielded value wins? Should it warn (probably)?
  4. Should the runtime have some safeguards in DEV if the iterator seems to go infinite ? (warnings and stop after a fixed amount of steps?)
  5. Hopefully one day Vetur will be able to typecheck the templates. This RFC would make typing the returned setup state much harder.
  6. Similar to 5: the TS typing of components is already quite complex, is this new setup signature even gonna mesh well with existing types? (hopefully but not a given)
  7. Possible drawback: it becomes harder for developers to see the shape of the state object as it can be spread across many places in a long setup function, possibly inside conditionals.
edimitchel commented 4 years ago

Thanks for your answer @jods4 !

As I said, returning data with yield goal is to get a more concise code. Your example was not relevant IMHO. Yes, in some way yield version is not shorter than traditional return. But I think returning value when we import create a nice and clean experience : about versioning, we don't change the return part, only the body of the setup function. Also, as I mention, I adressed a proposal to TC39 for being able to extract directly a destructured object.

About typing, I check it and types are well infered : image Yes, Vetur have to handle this specific case, but seriously, add each field I want to the TRC return is painful. Always do 2 actions for adding one feature is not a good experience.

Also, spreading directly a feature in the TRC is not a good practice, extracting field is more explicit.

Personally, this RFC should be added to the core because this adds a better experience and more concise code in general.

jods4 commented 4 years ago

To be clear, I'm not arguing against the idea of using yield to build state. I am just thinking it belongs more in a lib rather than inside core. You can write defineYieldComponent and use this today if you want to.

Your example was not relevant IMHO

Well, I copy-pasted the one from the RFC ;)

I adressed a proposal to TC39 for being able to extract directly a destructured object.

Don't hold your breath for JS changes. It usually takes years... that is, if it goes through. I personally don't think Vue should be designed with anything less than stage 3 in mind. If you want an example of something that's been desired for 6+ years by frameworks and has totally changed on the way and is still not stage 3, look no further than decorators.

About typing, I check it and types are well infered :

I have no doubt about setup type being inferred properly. It's the whole component + defineComponent thing that worries me.

edimitchel commented 4 years ago

I definitely agree with you ;) I'll improve my RFC example ;)