A comprehensive understanding of Vue.js functional components

A comprehensive understanding of Vue.js functional components

Preface

If you are a front-end developer and have read some Java code on some occasions, you may have seen a way of writing similar to the arrow function in ES6 syntax.

(String a, String b) -> a.toLowerCase() + b.toLowerCase();

This lambda expression, which appeared after Java 8, appears in C++/Python. It is more compact than traditional OOP-style code. Although this expression in Java is essentially a functional interface syntactic sugar that generates class instances, its concise writing and the behavior of processing immutable values ​​and mapping them to another value are typical functional programming (FP) features.

Butler Lampson, the 1992 Turing Award winner, made a famous statement:

All problems in computer science can be solved by another level of indirection
Any problem in computer science can be solved by adding a level of indirection

The “level of indirection” in this sentence is often translated as “level of abstraction,” and although some have debated its rigor, it makes sense regardless of the translation. In any case, the embrace of FP by OOP languages ​​is a direct reflection of the increasing integration and emphasis on functional programming in the programming field, and also confirms the "fundamental theorem of software engineering" that practical problems can be solved by introducing another level of indirection.

There is another popular saying that is not necessarily so rigorous:

OOP is an abstraction of data, while FP is used to abstract behavior

Different from object-oriented programming, which abstracts various objects and pays attention to the decoupling issues between them, functional programming focuses on the smallest single operation, turning complex tasks into repeated superposition of function operations of the type f(x) = y. Functions are first-class objects in FP and can be used as function parameters or returned by functions.

At the same time, in FP, functions should not depend on or affect external states, which means that for a given input, the same output will be produced - this is why words such as "immutable" and "pure" are often used in FP; if you also mention the "lambda calculus" and "curring" mentioned earlier, you will sound like a FP enthusiast.

The above concepts and their related theories were born in the first half of the 20th century. Many scientists have reaped fruitful results in their research on mathematical logic. Even the popular ML and AI have benefited from these results. For example, Haskell Curry, a master Polish-American mathematician at the time, left his name in typical functional practices such as the Haskell language and currying.

React Functional Components

If you have used the "chain syntax" of jQuery/RxJS, it can actually be regarded as the practice of monad in FP; and in recent years, most front-end developers have really come into contact with FP, one is from the functional-style Array instance methods such as map/reduce introduced in ES6, and the other is from the functional component (FC - functional component) in React.

Functional components in React are often called stateless components. A more intuitive name is render function, because it is really just a function used for rendering:

const Welcome = (props) => { 
  return <h1>Hello, {props.name}</h1>; 
}

In combination with TypeScript, you can also use type and FC<propsType> to constrain the input parameters of this function that returns jsx:

type GreetingProps = {
 name: string;
}
 
const Greeting:React.FC<GreetingProps> = ({ name }) => {
 return <h1>Hello {name}</h1>
};

You can also use interfaces and paradigms to define props types more flexibly:

interface IGreeting<T = 'm' | 'f'> {
 name: string;
 gender: T
}
export const Greeting = ({ name, gender }: IGreeting<0 | 1>): JSX.Element => {
 return <h1>Hello { gender === 0 ? 'Ms.' : 'Mr.' } {name}</h1>
};

Functional components in Vue (2.x)

In the [Functional Components] section of the Vue official website documentation, it is described as follows:

...we can mark a component as functional, which means it is stateless (no reactive data) and has no instance (no this context). A functional component looks like this:
 
Vue.component('my-component', {
  functional: true,
  // Props is optional props: {
    // ...
  },
  // To make up for the lack of an instance // provide a second argument as the context render: function (createElement, context) {
    // ...
  }
})
 
...
 
In version 2.5.0 and above, if you use [single file components], template-based functional components can be declared like this:
 
<template functional>
</template>

Developers who have written React and read this document for the first time may subconsciously exclaim, "Ah, this..." Just writing the word functional is called functional? ? ?

In fact, in Vue 3.x, you can actually write "functional components" that are pure rendering functions just like React . We'll talk about this later.

In the more common Vue 2.x, as stated in the documentation, a functional component (FC) means a component without an instance (no this context, no lifecycle methods, no listening to any properties, no management of any state) . From the outside, it can probably be seen as fc(props) => VNode function that just accepts some props and returns some kind of rendering result as expected.

Moreover, the real FP function is based on immutable state, and the "functional" components in Vue are not so ideal - the latter are based on variable data, and compared with ordinary components, they just have no instance concept. But its advantages are still obvious:

Because functional components ignore implementation logic such as lifecycle and monitoring, rendering overhead is very low and execution speed is fast

Compared with v-if and other instructions in ordinary components, using h function or combining jsx logic is clearer

Easier to implement the HOC (higher-order component) pattern, a container component that encapsulates some logic and conditionally renders parametric child components

Multiple root nodes can be returned via an array

🌰 Example: Optimizing custom columns in el-table

Let's first intuitively experience a typical scenario where FC is applicable:

551072df36b6159d4aa452536c412f12.png

This is an example of a custom table column given on the ElementUI official website. The corresponding template code is:

<template>
  <el-table
    :data="tableData"
    style="width: 100%">
    <el-table-column
      label="Date"
      width="180">
      <template slot-scope="scope">
        <i class="el-icon-time"></i>
        <span style="margin-left: 10px">{{ scope.row.date }}</span>
      </template>
    </el-table-column>
    <el-table-column
      label="Name"
      width="180">
      <template slot-scope="scope">
        <el-popover trigger="hover" placement="top">
          <p>Name: {{ scope.row.name }}</p>
          <p>Address: {{ scope.row.address }}</p>
          <div slot="reference" class="name-wrapper">
            <el-tag size="medium">{{ scope.row.name }}</el-tag>
          </div>
        </el-popover>
      </template>
    </el-table-column>
    <el-table-column label="operation">
      <template slot-scope="scope">
        <el-button
          size="mini"
          @click="handleEdit(scope.$index, scope.row)">Edit</el-button>
        <el-button
          size="mini"
          type="danger"
          @click="handleDelete(scope.$index, scope.row)">Delete</el-button>
      </template>
    </el-table-column>
  </el-table>
</template>

In actual business needs, small tables like the ones in the document examples certainly exist, but they are not the focus of our attention. ElementUI custom table columns are widely used in the rendering logic of large reports with numerous fields and complex interactions. They usually start with more than 20 columns, and each column contains a list of pictures, video preview pop-ups, paragraphs that need to be combined and formatted, and an indefinite number of operation buttons depending on permissions or status. The related template part is often hundreds of lines or even more. In addition to being lengthy, it is also difficult to reuse similar logic in different columns.

As the TV series "Friends" said:

Welcome to the real world! It sucks - but you'll love it!

Vue single-file components do not provide solutions for splitting templates such as include - after all, there is enough syntax sugar, and it is best not to have it.

Developers with mysophobia will try to encapsulate complex column templates into independent components to solve this pain point; this is already very good, but it creates performance risks compared to the original writing method.

Recalling your overwhelming confidence when answering the question about how to optimize multi-layer node rendering during the interview 😼, we should obviously go a step further in this practice, so as to split the concerns and avoid performance issues. Functional components are a suitable solution in this scenario.

The first thing we tried was to "translate" the date column in the original template into a functional component DateCol.vue:

<template functional>
  <div>
    <i class="el-icon-time"></i>
    <span style="margin-left: 10px; color: blue;">{{ props.row.date }}</span>
  </div>
</template> 

0107d1f090b1cfde7c5c3a6312acd6c4.png

After importing in the container page, declare it in components and use it:

cddd2a771a92e3966399c2366293df6c.png

It is basically the same as before; the only problem is that it is limited by a single root element and has an extra layer of div, which can also be solved with vue-fragment etc.

Next we refactor the name column into NameCol.js:

export default {
  functional: true,
  render(h, {props}) {
    const {row} = props;
    return h('el-popover', {
        props: {trigger: "hover", placement: "top"},
        scopedSlots: {
          reference: () => h('div', {class: "name-wrapper"}, [
            h('el-tag', {props: {size: 'medium'}}, [row.name + '~'])
          ])
        }
      }, [
          h('p', null, [`Name: ${ row.name }`]),
          h('p', null, [`Address: ${ row.address }`])
      ])
  }
} 

c9bcf10e1dd5fb7cf6598fc80743f28d.png

b93923ff0aa5cb73d08a003bb644ea65.png

The effect is amazing, and the use of arrays circumvents the limitation of a single root element; more importantly, this abstracted widget is a real js module . You can put it into a .js file instead of wrapping it with <script> , and you can freely do whatever you want .

h function may bring some extra mental burden, but as long as it is configured with JSX support, it will be almost the same as the original version.

In addition, we will talk about the scopedSlots involved here and the event handling that will be faced in the third column later.

Rendering Context

Recall from the documentation section mentioned above that the render function is of the form:

render: function (createElement, context) {}

In actual coding, createElement is usually written as h, and even if h is not called in jsx usage, it still needs to be written; in Vue3, you can use import { h } from 'vue' to import it globally.

It comes from the term "hyperscript", which is commonly used in many virtual-dom implementations. "Hyperscript" itself stands for "script that generates HTML structures" because HTML is the acronym for "hyper-text markup language". -- Evan You

The official website document continues:

Everything a component needs is passed in as the context parameter, which is an object with the following fields:

props: an object providing all props
children: array of VNode child nodes
slots: A function that returns an object containing all slots
scopedSlots: (2.6.0+) An object that exposes the passed in scoped slots. Also exposes normal slots as functions.
data: The entire data object passed to the component, passed into the component as the second parameter of createElement
parent: a reference to the parent component
listeners: (2.3.0+) An object containing all event listeners registered by parent components for this component. This is an alias for data.on.
injections: (2.3.0+) If the inject option is used, this object contains the properties that should be injected.

This context is defined as an interface type of RenderContext. When initializing or updating components inside Vue, it is formed as follows:

9e29f71bf512349a61441dd4d13a4535.png

Mastering the various properties defined by the RenderContext interface is the basis for us to play with functional components.

template

In the previous example, we used a template with the functional attribute to abstract the logic of the date column in the table into an independent module.

This is also partially explained in the schematic diagram above. Vue's template is actually compiled into a rendering function , or the template and the explicit render function follow the same internal processing logic and are attached with properties such as $options .

In other words, when dealing with some complex logic, we can still use the power of js, such as habitually calling methods in templates, etc. - of course, this is not a real Vue component method:

b31e059aa65f9ab946da51dbb2831634.png

emit

There is no method like this.$emit() in functional components.

But event callbacks can still be handled normally, and what you need to use is the context.listeners property - as mentioned in the documentation, this is an alias for data.on. For example, in the previous example, we want to listen for clicks on the icon of the date column in the container page:

<date-col v-bind="scope" @icon-click="onDateClick" />

In DateCol.vue, trigger the event like this:

<i class="el-icon-time" 
      @click="() => listeners['icon-click'](props.row.date)">
    </i>

The only thing to note is that although the above method is sufficient for most situations, if multiple events with the same name are listened to externally, listeners will become an array ; so a relatively complete encapsulation method is:

/**
 * Event triggering method for functional components * @param {object} listeners - listeners object in context * @param {string} eventName - event name * @param {...any} args - several parameters * @returns {void} - None */
export const fEmit = (listeners, eventName, ...args) => {
    const cbk = listeners[eventName]
    if (_.isFunction(cbk)) cbk.apply(null, args)
    else if (_.isArray(cbk)) cbk.forEach(f => f.apply(null, args))
}

filter

<label>{ title | withColon }</label> filter syntax in traditional Vue templates no longer works in the return structure of h functions or jsx.

Fortunately, the originally defined filter function is also a normal function, so the equivalent way of writing it can be:

import filters from '@/filters';
 
const { withColon } = filters;
 
//...
 
// render returns jsx <label>{ withColon(title) }</label>

Slots

The method of using slots in the template part of ordinary components cannot be used in the render function of functional components, including in jsx mode.

In the previous example, when refactoring the name column into NameCol.js, the corresponding writing method has been demonstrated; let's look at an example of a skeleton screen component in ElementUI. For example, the common template usage is like this:

<el-skeleton :loading="skeLoading">
    real text
    <template slot="template">
      <p>loading content</p>
    </template>
</el-skeleton>

This actually involves two slots, default and template. When switched to the render function of the functional component, the corresponding writing method is:

export default {
  functional: true,
  props: ['ok'],
  render(h, {props}) {
    return h('el-skeleton' ,{
      props: {loading: props.ok},
      scopedSlots: {
        default: () => 'real text',
        template: () => h('p', null, ['loading context'])
      }
    }, null)
  }
}

If you encounter a scoped slot that passes attributes like v-bind:user="user" , you can use user as the input parameter of the slot function .

The official website documentation also mentions the comparison between slots() and children :

<my-functional-component>
  <p v-slot:foo>
    first
  </p>
  <p>second</p>
</my-functional-component>
 
For this component, children will give you two paragraph tags, while slots().default will only pass the second anonymous paragraph tag, and slots().foo will pass the first named paragraph tag. It has both children and slots(), so you can choose to make your component aware of a slot mechanism, or simply hand it off to other components by passing children.

provide / inject

In addition to the usage of injections mentioned in the documentation, please also note that provide / inject in Vue 2 is ultimately non-responsive.

If you have to use this method after evaluation, you can try vue-reactive-provide

HTML content

The jsx in Vue cannot support the v-html writing in the ordinary component template. The corresponding element attribute is domPropsInnerHTML, such as:

<strong class={type} domPropsInnerHTML={formatValue(item, type)} />

In the render method, the word is split again and written as follows:

h('p', {
      domProps: {
            innerHTML: '<h1>hello</h1>'
      }
})

It’s a bit more laborious to write anyway, but thankfully it’s easier to remember than dangerouslySetInnerHTML in React.

style

If you use pure .js / .ts components, the only problem may be that you can no longer enjoy the scoped styles in .vue components; referring to the situation of React, there are only a few ways to solve it:

  • Import external styles and use naming conventions such as BEM
  • Enable CSS Modules in vue-loader options and apply styleMod.foo in your component
  • Dynamically build style arrays or objects in the module and assign them to attributes
  • Use tool methods to dynamically build style classes:
const _insertCSS = css => {
    let $head = document.head || document.getElementsByTagName('head')[0];
    const style = document.createElement('style');
    style.setAttribute('type', 'text/css');
    if (style.styleSheet) {
        style.styleSheet.cssText = css;
    } else {
        style.appendChild(document.createTextNode(css));
    }
    $head.appendChild(style);
    $head = null;
};

TypeScript

Both React and Vue provide some means to verify the props type. However, these methods are a bit cumbersome to configure, and they are a bit too heavy for lightweight functional components.

As a strongly typed superset of JavaScript, TypeScript can be used to more accurately define and check props types, is easier to use, and has friendlier auto-prompts in VSCode or other development tools that support Vetur.

To combine Vue functional components with TS, as defined by interface RenderContext<Props> , for external input props, you can use a custom TypeScript interface to declare its structure, such as:

interface IProps {
 year: string;
 quarters: Array<'Q1' | 'Q2' | 'Q3' | 'Q4'>;
 note:
  content: string;
  auther: stir;
 }
}

Then specify the interface as the first generic type of RenderContext:

import Vue, { CreateElement, RenderContext } from 'vue';
 
...
 
export default Vue.extend({
  functional: true,
  render: (h: CreateElement, context: RenderContext<IProps>) => {
     console.log(context.props.year);
   //...
  }
});

Combining composition-api

Similar to the design purpose of React Hooks, Vue Composition API also brings responsive features, lifecycle concepts such as onMounted, and methods for managing side effects to functional components to a certain extent.

Here we only discuss a unique way of writing in composition-api - returning the render function in the setup() entry function:

For example, define a counter.js:

import { h, ref } from "@vue/composition-api";
 
export default {
  model: {
    prop: "value",
    event: "zouni"
  },
  props: {
    value: {
      type: Number,
      default: 0
    }
  },
  setup(props, { emit }) {
    const counter = ref(props.value);
    const increment = () => {
      emit("zouni", ++counter.value);
    };
 
    return () =>
      h("div", null, [h("button", { on: { click: increment } }, ["plus"])]);
  }
};

In the container page:

<el-input v-model="cValue" />
<counter v-model="cValue" /> 

fc3645b0441ee1865f2c5e0581aed9f6.gif

If you want to use it in conjunction with TypeScript, the only changes are:

  • import { defineComponent } from "@vue/composition-api";
  • export default defineComponent<IProps>({ component })

Unit Testing

If the strong typing support of TypeScript is used, the parameter types inside and outside the component will be better protected.

As for component logic, unit testing is still required to complete the construction of security scaffolding. At the same time, since functional components are generally relatively simple, writing tests is not difficult.

In practice, due to the difference between FC and ordinary components, there are still some minor issues that need to be paid attention to:

re-render

Since functional components only trigger a rendering when the props they pass in change, you cannot get the updated state by just calling nextTick() in the test case. You need to manually trigger a re-rendering :

it("Batch Select All", async () => {
    let result = mockData;
    // This actually simulates the process of updating components each time props are passed in from the outside // wrapper.setProps() cannot be called on a functional component
    const update = async () => {
      makeWrapper(
        {
          value: result
        },
        {
          listeners: {
            change: m => (result = m)
          }
        }
      );
      await localVue.nextTick();
    };
    await update();
    expect(wrapper.findAll("input")).toHaveLength(6);
 
    wrapper.find("tr.whole label").trigger("click");
    await update();
    expect(wrapper.findAll("input:checked")).toHaveLength(6);
 
    wrapper.find("tr.whole label").trigger("click");
    await update();
    expect(wrapper.findAll("input:checked")).toHaveLength(0);
 
    wrapper.find("tr.whole label").trigger("click");
    await update();
    wrapper.find("tbody>tr:nth-child(3)>td:nth-child(2)>ul>li:nth-child(4)>label").trigger("click");
    await update();
    expect(wrapper.find("tr.whole label input:checked").exists()).toBeFalsy();
  });

Multiple root nodes

One benefit of functional components is that they can return an array of elements, which is equivalent to returning multiple root nodes in render().

At this time, if you directly use shallowMount or other methods to load components in the test, an error will occur:

[Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node.

The solution is to encapsulate a packaging component :

import { mount } from '@vue/test-utils'
import Cell from '@/components/Cell'
 
const WrappedCell = {
  components: { Cell },
  template: `
    <div>
      <Cell v-bind="$attrs" v-on="$listeners" />
    </div>
  `
}
 
const wrapper = mount(WrappedCell, {
  propsData: {
    cellData: {
      category: 'foo',
      description: 'bar'
    }
  }
});
 
describe('Cell.vue', () => {
  it('should output two tds with category and description', () => {
    expect(wrapper.findAll('td')).toHaveLength(2);
    expect(wrapper.findAll('td').at(0).text()).toBe('foo');
    expect(wrapper.findAll('td').at(1).text()).toBe('bar');
  });
});

Fragment component

Another trick that can be used with FC is that for some common components that reference vue-fragment (generally used to solve multi-node problems), you can encapsulate a functional component to stub out the fragment component in its unit test, thereby reducing dependencies and facilitating testing:

let wrapper = null;
const makeWrapper = (props = null, opts = null) => {
  wrapper = mount(Comp, {
    localVue,
    propsData: {
      ...props
    },
    stubs:
      Fragment: {
        functional: true,
        render(h, { slots }) {
          return h("div", slots().default);
        }
      }
    },
    attachedToDocument: true,
    sync: false,
    ...opts
  });
};

Functional components in Vue 3

This part is basically consistent with our previous practice in composition-api. Let's roughly extract the statement from the new official website document:

True functional components

In Vue 3, all functional components are created using normal functions. In other words, there is no need to define the { functional: true } component option .

They will receive two parameters: props and context . context parameter is an object containing the component's attrs , slots , and emit properties.

Additionally, h is now imported globally instead of being provided implicitly in the render function:

import { h } from 'vue'
 
const DynamicHeading = (props, context) => {
  return h(`h${props.level}`, context.attrs, context.slots)
}
 
DynamicHeading.props = ['level']
 
export default DynamicHeading

Single file components

In 3.x, the performance difference between stateful and functional components has been greatly reduced and is negligible in most use cases. Therefore, the migration path for developers using functional on single-file components is to remove the attribute and rename all references to props to $props and attrs to $attrs :

<template>
  <component
    v-bind:is="`h${$props.level}`"
    v-bind="$attrs"
  />
</template>
 
<script>
export default {
  props: ['level']
}
</script>

The main differences are:

  1. Remove functional attribute from <template>
  2. listeners are now passed as part of $attrs and can be removed

Summarize

This is the end of this article about Vue.js functional components. For more relevant Vue.js functional components, please search for previous articles on 123WORDPRESS.COM or continue to browse the following related articles. I hope you will support 123WORDPRESS.COM in the future!

You may also be interested in:
  • A brief discussion on the best solution for VUE anti-shake and throttling (functional components)
  • Detailed application examples of Vue functional components
  • Vue functional components-you deserve it
  • A brief discussion on the use of Vue functional components

<<:  HTML page header code is completely clear

>>:  A detailed analysis and processing of MySQL alarms

Recommend

jQuery realizes image highlighting

It is very common to highlight images on a page. ...

Beginners learn some HTML tags (1)

Beginners can learn HTML by understanding some HT...

Use the sed command to modify the kv configuration file in Linux

sed is a character stream editor under Unix, that...

How to manually deploy war packages through tomcat9 on windows and linux

The results are different in Windows and Linux en...

React Fiber structure creation steps

Table of contents React Fiber Creation 1. Before ...

Detailed steps to install Sogou input method on Ubuntu 20.04

1. Install Fcitx input framework Related dependen...

Tips for using the docker inspect command

Description and Introduction Docker inspect is a ...

UDP connection object principle analysis and usage examples

I wrote a simple UDP server and client example be...

How to automatically deploy Linux system using PXE

Table of contents Background Configuring DHCP Edi...

A brief talk about React Router's history

If you want to understand React Router, you shoul...

Summarize how to optimize Nginx performance under high concurrency

Table of contents Features Advantages Installatio...

5 Commands to Use the Calculator in Linux Command Line

Hello everyone, I am Liang Xu. When using Linux, ...

Tips for turning pixels into comprehensive brand experiences

Editor: This article discusses the role that inte...

Tutorial on deploying nginx+uwsgi in Django project under Centos8

1. Virtual environment virtualenv installation 1....

js to achieve simple image drag effect

This article shares the specific code of js to ac...