DEV Community

v-moe
v-moe

Posted on

Two-way data binding for concise APIs in Vue. The dialog example.

Two-way data binding is a controversial thing. Keeping the flow of data to be only from parent to child, is a beneficial thing when the time comes when you have to fix a bug in a complex application.
React has always been very strict about this, which leads to a pattern seen very often, for example in the popular Material UI library.

Imagine the situation where you want to create a component library, with a dialog component.

For simplicity our example will only have a button to close the dialog.

The parent component will have an additional button to toggle the open state of the dialog and the dialog component will be able to "close itself".

This is typically how this component would be used in React.

import React, { useState } from 'react'
import EasyDialog from './components/EasyDialog'

function App() {
  const [open, setOpen] = useState(true)
  return (
    <div>
      <button
        onClick={() => {
          setOpen(!open)
        }}
      >
        Toggle the dialog
      </button>
      <EasyDialog
        open={open}
        onClose={() => {
          setOpen(false)
        }}
      />
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

And this would be the component's code:

import React from 'react'

function EasyDialog({ open, onClose }) {
  return (
    open && (
      <div>
        <button onClick={onClose}>Close me</button>
      </div>
    )
  )
}

export default EasyDialog
Enter fullscreen mode Exit fullscreen mode

It feels wrong that you have to tell the dialog to call onClose, a prop passed down from the App component to the dialog, doesn't it?

In my honest opinion this boilerplate is even worse than two-way data binding, here the child is de facto executing a method from the parent!

In Vue this problem does not exist, since we can create our custom two-way data bindings, also known as custom v-models.

This is how the equivalent app looks in Vue

<script setup>
import { ref } from 'vue'
import EasyDialog from './components/EasyDialog.vue'

const open = ref(true)
</script>

<template>
  <div>
    <button @click="open = !open">Toggle the dialog</button>
    <EasyDialog v-model="open" />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Isn't this API a lot nicer? No need to pass any function to the child component, it's the child that emits:

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
  <div v-if="modelValue">
    <button @click="$emit('update:modelValue', false)">Close me</button>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

So much more concise ...
Vue allows to do it, Svelte allows to do it, React probably never will introduce such ergonomics.

The React team has its reasons to do so and most React developers feel comfortable with this approach, so as a Vue developer I sure won't question this decision and just be a happy guy :D

Discussion (0)