diff --git a/docs/rules/README.md b/docs/rules/README.md
index 92674e1f5..0fd8e2fa1 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -325,6 +325,7 @@ For example:
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: |
| [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md) | enforce v-on event naming style on custom components in template | :wrench: |
| [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: |
+| [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: |
### Extension Rules
diff --git a/docs/rules/valid-next-tick.md b/docs/rules/valid-next-tick.md
new file mode 100644
index 000000000..076fcc792
--- /dev/null
+++ b/docs/rules/valid-next-tick.md
@@ -0,0 +1,88 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/valid-next-tick
+description: enforce valid `nextTick` function calls
+---
+# vue/valid-next-tick
+
+> enforce valid `nextTick` function calls
+
+- :exclamation: ***This rule has not been released yet.***
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+Calling `Vue.nextTick` or `vm.$nextTick` without passing a callback and without awaiting the returned Promise is likely a mistake (probably a missing `await`).
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [`Vue.nextTick` API in Vue 2](https://vuejs.org/v2/api/#Vue-nextTick)
+- [`vm.$nextTick` API in Vue 2](https://vuejs.org/v2/api/#vm-nextTick)
+- [Global API Treeshaking](https://v3.vuejs.org/guide/migration/global-api-treeshaking.html)
+- [Global `nextTick` API in Vue 3](https://v3.vuejs.org/api/global-api.html#nexttick)
+- [Instance `$nextTick` API in Vue 3](https://v3.vuejs.org/api/instance-methods.html#nexttick)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-next-tick.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-next-tick.js)
diff --git a/lib/index.js b/lib/index.js
index ec311919d..2d600117d 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -164,6 +164,7 @@ module.exports = {
'v-on-function-call': require('./rules/v-on-function-call'),
'v-on-style': require('./rules/v-on-style'),
'v-slot-style': require('./rules/v-slot-style'),
+ 'valid-next-tick': require('./rules/valid-next-tick'),
'valid-template-root': require('./rules/valid-template-root'),
'valid-v-bind-sync': require('./rules/valid-v-bind-sync'),
'valid-v-bind': require('./rules/valid-v-bind'),
diff --git a/lib/rules/valid-next-tick.js b/lib/rules/valid-next-tick.js
new file mode 100644
index 000000000..4e41abbf2
--- /dev/null
+++ b/lib/rules/valid-next-tick.js
@@ -0,0 +1,204 @@
+/**
+ * @fileoverview enforce valid `nextTick` function calls
+ * @author Flo Edelmann
+ * @copyright 2021 Flo Edelmann. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const { findVariable } = require('eslint-utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * @param {Identifier} identifier
+ * @param {RuleContext} context
+ * @returns {ASTNode|undefined}
+ */
+function getVueNextTickNode(identifier, context) {
+ // Instance API: this.$nextTick()
+ if (
+ identifier.name === '$nextTick' &&
+ identifier.parent.type === 'MemberExpression' &&
+ utils.isThis(identifier.parent.object, context)
+ ) {
+ return identifier.parent
+ }
+
+ // Vue 2 Global API: Vue.nextTick()
+ if (
+ identifier.name === 'nextTick' &&
+ identifier.parent.type === 'MemberExpression' &&
+ identifier.parent.object.type === 'Identifier' &&
+ identifier.parent.object.name === 'Vue'
+ ) {
+ return identifier.parent
+ }
+
+ // Vue 3 Global API: import { nextTick as nt } from 'vue'; nt()
+ const variable = findVariable(context.getScope(), identifier)
+
+ if (variable != null && variable.defs.length === 1) {
+ const def = variable.defs[0]
+ if (
+ def.type === 'ImportBinding' &&
+ def.node.type === 'ImportSpecifier' &&
+ def.node.imported.type === 'Identifier' &&
+ def.node.imported.name === 'nextTick' &&
+ def.node.parent.type === 'ImportDeclaration' &&
+ def.node.parent.source.value === 'vue'
+ ) {
+ return identifier
+ }
+ }
+
+ return undefined
+}
+
+/**
+ * @param {CallExpression} callExpression
+ * @returns {boolean}
+ */
+function isAwaitedPromise(callExpression) {
+ if (callExpression.parent.type === 'AwaitExpression') {
+ // cases like `await nextTick()`
+ return true
+ }
+
+ if (callExpression.parent.type === 'ReturnStatement') {
+ // cases like `return nextTick()`
+ return true
+ }
+
+ if (
+ callExpression.parent.type === 'MemberExpression' &&
+ callExpression.parent.property.type === 'Identifier' &&
+ callExpression.parent.property.name === 'then'
+ ) {
+ // cases like `nextTick().then()`
+ return true
+ }
+
+ if (
+ callExpression.parent.type === 'VariableDeclarator' ||
+ callExpression.parent.type === 'AssignmentExpression'
+ ) {
+ // cases like `let foo = nextTick()` or `foo = nextTick()`
+ return true
+ }
+
+ if (
+ callExpression.parent.type === 'ArrayExpression' &&
+ callExpression.parent.parent.type === 'CallExpression' &&
+ callExpression.parent.parent.callee.type === 'MemberExpression' &&
+ callExpression.parent.parent.callee.object.type === 'Identifier' &&
+ callExpression.parent.parent.callee.object.name === 'Promise' &&
+ callExpression.parent.parent.callee.property.type === 'Identifier'
+ ) {
+ // cases like `Promise.all([nextTick()])`
+ return true
+ }
+
+ return false
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce valid `nextTick` function calls',
+ // categories: ['vue3-essential', 'essential'],
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/valid-next-tick.html'
+ },
+ fixable: 'code',
+ schema: []
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ return utils.defineVueVisitor(context, {
+ /** @param {Identifier} node */
+ Identifier(node) {
+ const nextTickNode = getVueNextTickNode(node, context)
+ if (!nextTickNode || !nextTickNode.parent) {
+ return
+ }
+
+ const parentNode = nextTickNode.parent
+
+ if (
+ parentNode.type === 'CallExpression' &&
+ parentNode.callee !== nextTickNode
+ ) {
+ // cases like `foo.then(nextTick)` are allowed
+ return
+ }
+
+ if (
+ parentNode.type === 'VariableDeclarator' ||
+ parentNode.type === 'AssignmentExpression'
+ ) {
+ // cases like `let foo = nextTick` or `foo = nextTick` are allowed
+ return
+ }
+
+ if (parentNode.type !== 'CallExpression') {
+ context.report({
+ node,
+ message: '`nextTick` is a function.',
+ fix(fixer) {
+ return fixer.insertTextAfter(node, '()')
+ }
+ })
+ return
+ }
+
+ if (parentNode.arguments.length === 0) {
+ if (!isAwaitedPromise(parentNode)) {
+ context.report({
+ node,
+ message:
+ 'Await the Promise returned by `nextTick` or pass a callback function.',
+ suggest: [
+ {
+ desc: 'Add missing `await` statement.',
+ fix(fixer) {
+ return fixer.insertTextBefore(parentNode, 'await ')
+ }
+ }
+ ]
+ })
+ }
+ return
+ }
+
+ if (parentNode.arguments.length > 1) {
+ context.report({
+ node,
+ message: '`nextTick` expects zero or one parameters.'
+ })
+ return
+ }
+
+ if (isAwaitedPromise(parentNode)) {
+ context.report({
+ node,
+ message:
+ 'Either await the Promise or pass a callback function to `nextTick`.'
+ })
+ }
+ }
+ })
+ }
+}
diff --git a/tests/lib/rules/valid-next-tick.js b/tests/lib/rules/valid-next-tick.js
new file mode 100644
index 000000000..220e43417
--- /dev/null
+++ b/tests/lib/rules/valid-next-tick.js
@@ -0,0 +1,405 @@
+/**
+ * @fileoverview enforce valid `nextTick` function calls
+ * @author Flo Edelmann
+ * @copyright 2021 Flo Edelmann. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/valid-next-tick')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: {
+ ecmaVersion: 2017,
+ sourceType: 'module'
+ }
+})
+
+tester.run('valid-next-tick', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: ''
+ },
+ {
+ filename: 'test.vue',
+ code: ``
+ },
+
+ // https://github.com/vuejs/eslint-plugin-vue/pull/1404#discussion_r550937500
+ {
+ filename: 'test.vue',
+ code: ``
+ },
+
+ // https://github.com/vuejs/eslint-plugin-vue/pull/1404#discussion_r550936410
+ {
+ filename: 'test.vue',
+ code: ``
+ },
+
+ // https://github.com/vuejs/eslint-plugin-vue/pull/1404#discussion_r550936933
+ {
+ filename: 'test.vue',
+ code: ``
+ },
+
+ // https://github.com/vuejs/eslint-plugin-vue/pull/1404#discussion_r551769969
+ {
+ filename: 'test.vue',
+ code: ``
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: ``,
+ output: null,
+ errors: [
+ {
+ message:
+ 'Await the Promise returned by `nextTick` or pass a callback function.',
+ line: 4,
+ column: 11,
+ suggestions: [
+ {
+ output: ``
+ }
+ ]
+ },
+ {
+ message:
+ 'Await the Promise returned by `nextTick` or pass a callback function.',
+ line: 5,
+ column: 15,
+ suggestions: [
+ {
+ output: ``
+ }
+ ]
+ },
+ {
+ message:
+ 'Await the Promise returned by `nextTick` or pass a callback function.',
+ line: 6,
+ column: 16,
+ suggestions: [
+ {
+ output: ``
+ }
+ ]
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: ``,
+ output: ``,
+ errors: [
+ {
+ message: '`nextTick` is a function.',
+ line: 4,
+ column: 11
+ },
+ {
+ message: '`nextTick` is a function.',
+ line: 5,
+ column: 15
+ },
+ {
+ message: '`nextTick` is a function.',
+ line: 6,
+ column: 16
+ },
+ {
+ message: '`nextTick` is a function.',
+ line: 8,
+ column: 11
+ },
+ {
+ message: '`nextTick` is a function.',
+ line: 9,
+ column: 15
+ },
+ {
+ message: '`nextTick` is a function.',
+ line: 10,
+ column: 16
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: ``,
+ output: ``,
+ errors: [
+ {
+ message: '`nextTick` is a function.',
+ line: 4,
+ column: 17
+ },
+ {
+ message: '`nextTick` is a function.',
+ line: 5,
+ column: 21
+ },
+ {
+ message: '`nextTick` is a function.',
+ line: 6,
+ column: 23
+ }
+ ]
+ },
+
+ // https://github.com/vuejs/eslint-plugin-vue/pull/1404#discussion_r550936933
+ {
+ filename: 'test.vue',
+ code: ``,
+ output: ``,
+ errors: [
+ {
+ message: '`nextTick` is a function.',
+ line: 4,
+ column: 24
+ },
+ {
+ message: '`nextTick` is a function.',
+ line: 5,
+ column: 28
+ },
+ {
+ message: '`nextTick` is a function.',
+ line: 6,
+ column: 29
+ }
+ ]
+ },
+
+ {
+ filename: 'test.vue',
+ code: ``,
+ output: null,
+ errors: [
+ {
+ message: '`nextTick` expects zero or one parameters.',
+ line: 4,
+ column: 11
+ },
+ {
+ message: '`nextTick` expects zero or one parameters.',
+ line: 5,
+ column: 15
+ },
+ {
+ message: '`nextTick` expects zero or one parameters.',
+ line: 6,
+ column: 16
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: ``,
+ output: null,
+ errors: [
+ {
+ message:
+ 'Either await the Promise or pass a callback function to `nextTick`.',
+ line: 4,
+ column: 11
+ },
+ {
+ message:
+ 'Either await the Promise or pass a callback function to `nextTick`.',
+ line: 5,
+ column: 15
+ },
+ {
+ message:
+ 'Either await the Promise or pass a callback function to `nextTick`.',
+ line: 6,
+ column: 16
+ },
+ {
+ message:
+ 'Either await the Promise or pass a callback function to `nextTick`.',
+ line: 8,
+ column: 17
+ },
+ {
+ message:
+ 'Either await the Promise or pass a callback function to `nextTick`.',
+ line: 9,
+ column: 21
+ },
+ {
+ message:
+ 'Either await the Promise or pass a callback function to `nextTick`.',
+ line: 10,
+ column: 22
+ }
+ ]
+ }
+ ]
+})