Abdelrahman Awad

The case for HOC vs The Composition API

Published 6 May, 2020|

The new composition API is shaking up the Vue community and is taking it by storm. Everyone is making the case for it and how it makes for a better pattern to re-use logic. But would the higher-order components (HOC) still be viable after its introduction with Vue 3 release?

Composition API Primer

There is no shortage of videos and talks about the new Vue 3.0 composition API that's inspired by React hooks. I will assume you are already up to speed on that trend.

Scoped slots are Vue's way to compose components logic together and it is very elegant and is underused by the majority of the Vue ecosystem.

There are two reasons for that:

  • The template-based default approach of Vue.js and its ease of use doesn't force developers into advanced patterns like HOC.
  • Vue still has mixins which solve the problem albeit badly in most cases, I don't see it used a lot though.

Here is an example of the famous mouse position example that started it all, first in the declarative approach where we use scoped slots (Vue's HOC):

<template>
  <MousePosition v-slot="{ x, y }">
    <span>{{ x }}</span>
    <span>{{ y }}</span>
  </MousePosition>
</template>

<script>
  import MousePosition from '@/components/MousePosition';

  export default {
    components: {
      MousePosition
    }
  };
</script>

The implementation details are not our focus here. The MousePosition component exposes the mouse position using slot props. The composition API introduces a different approach, a more declarative one.

<template>
  <div>
    <span>{{ x }}</span>
    <span>{{ y }}</span>
  </div>
</template>

<script>
  import { useMousePosition } from '@/maybeHooks/mouse-position';

  export default {
    setup() {
      const { x, y } = useMousePosition();

      return { x, y };
    }
  };
</script>

The composition API isn't less verbose than its HOC counterpart. But in that specific example, it moves some of the template verbosity into the script. In other words, it converts that logic from being declarative to being imperative.

That might not mean a lot but this is the most crucial idea if we are going to find a case for HOCs. Let's explore some more examples to see if we can gather more insights.

Extendibility

Let's start with something common for most of us, calling APIs. Assuming we have both a Fetch component and a useFetch composition function and we want to view a list of users sorted by their names. If we would go with the declarative style:

<template>
  <Fetch endpoint="/api/users" v-slot="{ data }">
    <ul>
      <li
        v-for="user in [...((data && data.users) || [])].sort((a, b) => {
          /* sorting logic */
        })"
        :key="user.id"
      >
        {{ user.name }}
      </li>
    </ul>
  </Fetch>
</template>

<script>
  import Fetch from '@/components/Fetch';

  export default {
    components: {
      Fetch
    }
  };
</script>

That was ugly. first, we had to shallow clone the array of users because we don't want to mutate slot props. Also since we have no access to the data outside of the component scoped slot we needed to inline our sorting function.

Thankfully, since we set our key attribute here it is efficient to some extent. What about the imperative style, is it any better?

<template>
  <ul>
    <li v-for="user in users">{{ user.name }}</li>
  </ul>
</template>

<script>
  import { computed } from 'vue';
  import { useFetch } from '@/maybeHooks/fetch';

  export default {
    setup() {
      const { data } = useFetch('/api/users');
      const users = computed(() => {
        if (!data.value) {
          return [];
        }

        return [...data.value.users].sort((a, b) => {
          // Sorting logic...
        });
      });

      return {
        users
      };
    }
  };
</script>

That is much better. We have the full force of JavaScript backing us up in the script tag, allowing us to do much more. For example if we would use lodash to refactor things a little bit, it would look like this:

import { sortBy } from 'lodash-es';

export default {
  setup() {
    const { data } = useFetch('/api/users');
    const users = computed(() => {
      if (!data.value) {
        return [];
      }

      return sortBy(data.value.users, ['name']);
    });

    return {
      users
    };
  }
};

Declaratively it would be very annoying since we have to expose our imported sortBy function:

<template>
  <Fetch endpoint="/api/users" v-slot="{ data }">
    <ul>
      <li v-for="user in sortBy(data.users, ['name'])" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
  </Fetch>
</template>

<script>
  import Fetch from '@/components/Fetch';
  import { sortBy } from 'lodash-es';

  export default {
    components: {
      Fetch
    },
    setup() {
      return {
        sortBy
      };
    }
  };
</script>

This is problematic and gives us a second insight. That the composition API gives us the full expressiveness of the JavaScript language. This has always been the argument for JSX over Templates.

So using the composition API here has more value than the HOC approach, so let's phrase a rule for it. We could say, HOCs are viable if you are not going to modify/extend the exposed props.

Reusability Revised

It's often argued that reusability for the composition functions are much higher than the HOC counterparts. Using the useMousePosition function as an example here, it is much simpler to import useMousePosition and use it throughout our components for example ComponentA and ComponentB and we would still have our flexibility intact.

But allow me to present another scenario than the one that favors the composition API, let's say you want to use the useMousePosition function many times within the same template/component.

Now it would look like this:

<template>
  <div>
    <span>{{ x }}</span>
    <span>{{ y }}</span>
    <span>{{ x2 }}</span>
    <span>{{ y2 }}</span>
    <span>{{ x3 }}</span>
    <span>{{ y3 }}</span>
  </div>
</template>

<script>
  import { useMousePosition } from '@/maybeHooks/mouse-position';

  export default {
    setup() {
      const { x, y } = useMousePosition();
      const { x: x2, y: y2 } = useMousePosition();
      const { x: x3, y: y3 } = useMousePosition();

      return { x, y, x2, y2, x3, y3 };
    }
  };
</script>

We immediately run into one of the most common problems, naming things. Now we have many x coordinate to name and since they all have the same scope, they need to be unique from one another. Meanwhile, HOCs don't suffer from this problem:

<template>
  <div>
    <MousePosition v-slot="{ x, y }">
      <span>{{ x }}</span>
      <span>{{ y }}</span>
    </MousePosition>

    <MousePosition v-slot="{ x, y }">
      <span>{{ x }}</span>
      <span>{{ y }}</span>
    </MousePosition>

    <MousePosition v-slot="{ x, y }">
      <span>{{ x }}</span>
      <span>{{ y }}</span>
    </MousePosition>
  </div>
</template>

<script>
  import MousePosition from '@/components/MousePosition';

  export default {
    components: {
      MousePosition
    }
  };
</script>

It's actually much better than the composition function approach. Since each scoped-slot has it's own isolated scope, they cannot conflict with one another. This gives us tiny namespaces in the template where we don't have to rename our stuff. Our takeaway here is that, while the composition API is very reusable, it's reusability can be questioned if the same function would be used multiple times within the same component/scope.

Of course, you could refactor things a little bit to favor the composition API, like returning an array instead of an object:

import { useMousePosition } from '@/maybeHooks/mouse-position';

export default {
  setup() {
    const [x, y] = useMousePosition();
    const [x2, y2] = useMousePosition();
    const [x3, y3] = useMousePosition();

    return { x, y, x2, y2, x3, y3 };
  }
};

Which is slightly better, but I still think the HOC approach here is much easier to digest and maintain. This brings us to another conclusion about the composition API. The smaller and more specialized your components are, the better you would make use of the composition API.

However if you have really big components, the composition API isn't any better than its counterpart.

Case Study: VeeValidate v4

I have been working on VeeValidate v4 for a while now, and while the progress is slow. I think vee-validate being one of the best representatives of the declarative style of things illustrates the contrast between using HOCs and composition functions best.

So vee-validate v4 while not released yet, offers both options, allowing you to use whatever style you need. It exposes both a ValidationProvider component and useField composition function.

Borrowing some insights from React's popular library Formik, I have noticed people still using the declarative approach over the imperative one. Even though both could achieve the same thing. Here is how to build a registration form using useField which is the imperative option:

<template>
  <div>
    <form @submit.prevent="onSubmit">
      <input name="name" v-model="name" type="text" />
      <span>{{ nameErrors[0] }}</span>

      <input name="email" v-model="email" type="email" />
      <span>{{ emailErrors[0] }}</span>

      <input name="password" v-model="password" type="password" />
      <span>{{ passwordErrors[0] }}</span>

      <button>Submit</button>
    </form>
  </div>
</template>

<script>
  import { useField, useForm } from 'vee-validate';

  export default {
    setup() {
      const { form, handleSubmit } = useForm();
      const { value: name, errors: nameErrors } = useField('name', 'required', { form });
      const { value: password, errors: passwordErrors } = useField('password', 'required|min:8', { form });
      const { value: email, errors: emailErrors } = useField('email', 'required|email', { form });
      const onSubmit = handleSubmit(values => {
        // TODO: Send data to the API
      });

      return {
        name,
        nameErrors,
        password,
        passwordErrors,
        email,
        emailErrors,
        onSubmit
      };
    }
  };
</script>

This isn't so bad, While useForm and useField save you the trouble of creating ref for your models, it's still too much to deal with in the setup function. If you were to have a very complicated form this will get much worse.

Now here is how the declarative style fares:

<template>
  <div>
    <ValidationObserver as="form" @submit="onSubmit" v-slot="{ errors }">
      <ValidationProvider name="name" rules="required" as="input" />
      <span>{{ errors.name }}</span>

      <ValidationProvider name="email" rules="required|email" as="input" type="email" />
      <span>{{ errors.email }}</span>

      <ValidationProvider name="password" rules="required|min:8" as="input" type="password" />
      <span>{{ errors.password }}</span>

      <button>Submit</button>
    </ValidationObserver>
  </div>
</template>

<script>
  import { ValidationProvider, ValidationObserver } from 'vee-validate';

  export default {
    components: {
      ValidationProvider,
      ValidationObserver
    },
    setup() {
      function onSubmit(values) {
        // TODO: Send stuff to API
      }

      return {
        onSubmit
      };
    }
  };
</script>

I could be biased, but in my opinion that is indeed much better. It's easier to follow, this is actually one of the new ways vee-validate v4 is making use of HOCs to provide better experience for building forms than v3.

If you take a closer look, notice how much abstraction is being done with the components there. You no longer even have to specify a v-model or define them in your data/setup. You just declare "this is a field" and the submission handler automatically gives you access to those values which is what most of forms are.

You could still have full control and use scoped slots just like v3:

<template>
  <!-- ....... -->

  <ValidationProvider name="f2" rules="required" v-slot="{ field, errors }">
    <input type="text" v-bind="field" />
    <span>{{ errors[0] }}</span>
  </ValidationProvider>

  <!-- ....... -->
</template>

I will be publishing another article shortly about the changes to v4. The takeaway here is that because HOCs live in the template, they can infer assumptions about your code and they would be better informed than otherwise with the imperative approach.

So, when should you use useField since the HOC approach looks better here?

Following some of the takeaways we highlighted earlier, instead of building an entire form with useField. Reducing the scope to a more specialized component pays dividends here, let's say we want to create a TextInput component that can be validated:

<template>
  <div>
    <input type="text" v-model="value" />
    <span>{{ errors[0] }}</span>
  </div>
</template>

<script>
  import { watch } from 'vue';
  import { useField } from 'vee-validate';

  export default {
    name: 'TextInput',
    setup(props, { emit }) {
      const { errors, value } = useField(props.name, props.rules);
      watch(value, val => {
        emit('modelUpdate', val);
      });

      return {
        errors,
        value
      };
    }
  };
</script>

You can use this component to build up your complex form. This component now can do a lot more than its HOC counterpart. We were able to make better use of the composition API because we reduced our component scope. If this matches your use-case then useField is the better approach here. For example if you are building a component library/system, the imperative approach is suited more.

You can think of the ValidationProvider as a high-level generic implementation of the useField functionality. Under the hood the ValidationProvider uses useField and abstracts away the rest.

This is actually an observation that I didn't hear anyone say it, the composition API is the best API to build HOCs. They complement one another, not replace each other.

Conclusion

Let's summarize the takeaways we've discovered in this article. I argue that HOCs are still very much viable, but the composition API allows for more fine-grained control over many of the cases where HOCs were the only solution but not the best solution.

You should use HOCs if:

  • If the slot props will not be modified/extended.
  • If you are going to use the same logic multiple times in the same component (Like forms).
  • If you are going to infer some information from the template. i.e: a11y and animations.

Otherwise, If your components are specialized, you can make better use of the composition API which requires more discipline than HOCs but it will pay dividends.

So instead of thinking "Composition API" is the new hip and should be always used, think of it as just a tool in your existing arsenal, in some areas it shines and in others its outshined by other tools like HOCs. And with an amazing framework like Vue.js you get to choose between them and mix and match in a very cohesive manner.

Subscribe for my latest content