React.js / Next.js and Vue.js / Nuxt.js Syntax Comparison Side by Side

Andrew
Vue.js Developers
Published in
10 min readApr 19, 2020
Photo by Polina Tankilevitch from Pexels

React.js and Vue.js are both great frameworks. And Next.js and Nuxt.js even bring them to the next level, which helps us to create the application with less configuration and better maintainability. But if you have to switch between those to frameworks frequently. You might easily forget the syntax in another framework after diving into the other one. In this article, I summarize the basic syntax and scenarios for those frameworks and list then side by side. I hope this can help us catch up with the syntax as soon as possible.

Agenda

React.js vs Vue.js

Next.js vs Nuxt.js

Tools

Render

React.js

ReactDOM.render(<App />, document.getElementById("root"));

Vue.js

new Vue({
render: (h) => h(App),
}).$mount("#root");

Basic-Component

React.js

  • Class component
class MyReactComponent extends React.Component {
render() {
return <h1>Hello world</h1>;
}
}
  • Function component
function MyReactComponent() {
return <h1>Hello world</h1>;
}

Vue.js

<template>
<h1>Hello World</h1>
</template>
<script>
export default {
name: "MyVueComponent",
};
</script>

Prop

React.js

function MyReactComponent(props) {
const { name, mark } = props;
return <h1>Hello {name}{mark}</h1>;
}
MyReactComponent.propTypes = {
name: PropTypes.string.isRequired,
mark: PropTypes.string,
}
MyReactComponent.defaultProps = {
mark: '!',
}
...
<MyReactComponent name="world">

Vue.js

<template>
<h1>Hello {{ name }}</h1>
</template>
<script>
export default {
name: "MyVueComponent",
props: {
name: {
type: String,
required: true,
},
mark: {
type: String,
default: "!",
},
},
};
</script>
...<MyVueComponent name="World" />

Event-Binding

React.js

  • Class component
class MyReactComponent extends React.Component {
save = () => {
console.log("save");
};
render() {
return <button onClick={this.save}>Save</button>;
}
}
  • Function component
function MyReactComponent() {
const save = () => {
console.log("save");
};
return <button onClick={save}>Save</button>;
}

Vue.js

<template>
<button @click="save()">Save</button>
</template>
<script>
export default {
methods: {
save() {
console.log("save");
},
},
};
</script>

Custom-Event

React.js

function MyItem({ item, handleDelete }) {
return <button onClick={() => handleDelete(item)}>{item.name}</button>;
/*
* Apply useCallback hook to prevent generate new function every rendering.
*
* const handleClick = useCallback(() => handleDelete(item), [item, handleDelete]);
*
* return <button onClick={handleClick}>{item.name}</button>;
*/
}
...function App() {
const handleDelete = () => { ... }
return <MyItem item={...} handleDelete={handleDelete} />
}

Vue.js

<template>
<button @click="deleteItem()">{{item.name}}</button>
</template>
<script>
export default {
name: "my-item",
props: {
item: Object,
},
methods: {
deleteItem() {
this.$emit("delete", this.item);
},
},
};
</script>
...<template>
<MyItem :item="item" @delete="handleDelete" />
</template>
<script>
export default {
components: {
MyItem,
},
methods: {
handleDelete(item) { ... }
},
};
</script>

State

React.js

  • Class component
class MyReactComponent extends React.Component {
state = {
name: 'world,
}
render() {
return <h1>Hello { this.state.name }</h1>;
}
}
  • Function component
function MyReactComponent() {
const [name, setName] = useState("world");
return <h1>Hello {name}</h1>;
}

Vue.js

<template>
<h1>Hello {{ name }}</h1>
<!-- use component state as prop -->
<my-vue-component :name="name">
</template>
<script>
export default {
data() {
return { name: "world" };
},
};
</script>

Change-State

React.js

  • Class component
class MyReactComponent extends React.Component {
state = {
count: 0,
};
increaseCount = () => {
this.setState({ count: this.state.count + 1 });
// get current state before update to make sure we didn't use the stale value
// this.setState(currentState => ({ count: currentState.count + 1 }));
};
render() {
return (
<div>
<span>{this.state.count}</span>
<button onClick={this.increaseCount}>Add</button>
</div>
);
}
}
  • Function component
function MyReactComponent() {
const [count, setCount] = useState(0);
const increaseCount = () => {
setCount(count + 1);
// setCount(currentCount => currentCount + 1);
};
return (
<div>
<span>{count}</span>
<button onClick={increaseCount}>Add</button>
</div>
);
}

Vue.js

<template>
<div>
<span>{{count}}</span>
<button @click="increaseCount()">Add</button>
</div>
</template>
<script>
export default {
data() {
return { count: 0 };
},
methods: {
increaseCount() {
this.count = this.count + 1;
},
},
};
</script>

Two-Way-Binding (Vue.js only)

React.js

React didn’t have two-way binding, so we need to handle the data flow on our own

function MyReactComponent() {
const [content, setContent] = useState("");
return (
<input
type="text"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
);
}

Vue.js

<template>
<input type="text" v-model="content" />
</template>
<script>
export default {
data() {
return { content: "" };
},
};
</script>

Compute

React.js

React.js don’t have compute property, but we can achieve this through react hook easily

function DisplayName({ firstName, lastName }) {
const displayName = useMemo(() => {
return `${firstName} ${lastName}`;
}, [firstName, lastName]);
return <div>{displayName}</div>;
}
...<DisplayName firstName="Hello" lastName="World" />

Vue.js

<template>
<div>{{displayName}}</div>
</template>
<script>
export default {
name: "display-name",
props: {
firstName: String,
lastName: String,
},
computed: {
displayName: function () {
return `${this.firstName} ${this.lastName}`;
},
},
};
</script>
...<DisplayName firstName="Hello" lastName="World" />

Watch

React.js don’t have watch property, but we can achieve this through react hook easily

function MyReactComponent() {
const [count, setCount] = useState(0);
const increaseCount = () => {
setCount((currentCount) => currentCount + 1);
};
useEffect(() => {
localStorage.setItem("my_count", newCount);
}, [count]);
return (
<div>
<span>{count}</span>
<button onClick={increaseCount}>Add</button>
</div>
);
}

Vue.js

<template>
<div>
<span>{{count}}</span>
<button @click="increaseCount()">Add</button>
</div>
</template>
<script>
export default {
data() {
return { count: 0 };
},
methods: {
increaseCount() {
this.count = this.count + 1;
},
},
watch: {
count: function (newCount, oldCount) {
localStorage.setItem("my_count", newCount);
},
},
};
</script>

Children-and-Slot

React.js

function MyReactComponent({ children }) {
return <div>{children}</div>;
}
...<MyReactComponent>Hello World</MyReactComponent>

Vue.js

<template>
<div>
<slot />
</div>
</template>
<script>
export default {
name: "my-vue-component",
};
</script>
...<MyVueComponent>Hello World</MyVueComponent>

Render-HTML

React.js

function MyReactComponent() {
return <div dangerouslySetInnerHTML={{ __html: "<pre>...</pre>" }} />;
}

Vue.js

<template>
<div v-html="html"></div>
</template>
<script>
export default {
data() {
return {
html: "<pre>...</pre>",
};
},
};
</script>

Conditional-Rendering

React.js

function MyReactComponent() {
const [isLoading, setLoading] = useState(true);
return (
<div>
{isLoading && <span>Loading...</span>}
{isLoading ? <div>is loading</div> : <div>is loaded</div>}
</div>
);
}

Vue.js

<template>
<div>
<!--v-show: always render but change css base on the condition-->
<span v-show="loading">Loading...</span>
<div>
<div v-if="loading">is loading</div>
<div v-else>is loaded</div>
</div>
</div>
</template>
<script>
export default {
data() {
return { loading: true };
},
};
</script>

List-Rendering

React.js

function MyReactComponent({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name}: {item.desc}
</li>
))}
</ul>
);
}

Vue.js

<template>
<ul>
<li v-for="item in items" :key="item.id">
{{item.name}}: {{item.desc}}
</li>
</ul>
</template>
<script>
export default {
props: {
items: Array,
},
};
</script>

Render-Props

React.js

function Modal({children, isOpen}) {
const [isModalOpen, toggleModalOpen] = useState(isOpen);
return (
<div className={isModalOpen ? 'open' : 'close'}>
{type children === 'function' ? children(toggleModalOpen) : children}
</div>)
;
}
Modal.propTypes = {
isOpen: PropTypes.bool,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
}
Modal.defaultProps = {
isOpen: false,
}
...<Modal isOpen>
{(toggleModalOpen) => {
<div>
<div>...</div>
<button onClick={() => toggleModalOpen(false)}>Cancel</button>
</div>
}}
</Modal>

Vue.js (slot)

<template>
<div v-show="isModalOpen">
<slot v-bind:toggleModal="toggleModalOpen" />
</div>
</template>
<script>
export default {
name: "modal",
props: {
isOpen: {
type: Boolean,
default: false,
},
},
data() {
return {
isModalOpen: this.isOpen,
};
},
methods: {
toggleModalOpen(state) {
this.isModalOpen = state;
},
},
};
</script>
...<Modal isOpen>
<template v-slot="slotProps">
<div>...</div>
<button @click="slotProps.toggleModal(false)">Close</button>
</template>
</Modal>

Lifecycle

React.js

http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

  • Class component
class MyReactComponent extends React.Component {
static getDerivedStateFromProps(props, state) {}
componentDidMount() {}
shouldComponentUpdate(nextProps, nextState) {}
getSnapshotBeforeUpdate(prevProps, prevState) {}
componentDidUpdate(prevProps, prevState) {}
componentWillUnmount() {}
render() {
return <div>Hello World</div>;
}
}
  • Function component
function MyReactComponent() {
// componentDidMount
useEffect(() => {}, []);
// componentDidUpdate + componentDidMount
useEffect(() => {});
// componentWillUnmount
useEffect(() => {
return () => {...}
}, []);
// runs synchronously after a render but before the screen is updated
useLayoutEffect(() => {}, []);
return <div>Hello World</div>;
}

Vue.js

<template>
<div>Hello World</div>
</template>
<script>
export default {
beforeCreate() {},
created() {},
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeDestroy() {},
destroyed() {},
};
</script>

Error-Handling

React.js

class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {} render() {
if (this.state.hasError) return <h1>Something went wrong.</h1>;
return this.props.children;
}
}
...<ErrorBoundary>
<App />
</ErrorBoundary>

Vue.js

const vm = new Vue({
data: {
error: "",
},
errorCaptured: function(err, component, details) {
error = err.toString();
}
}

Ref

React.js

  • Class component
class AutofocusInput extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
}
state = {
content: "",
};
componentDidMount() {
this.ref.current.focus();
}
setContent = (e) => {
this.setState({ content: e.target.value });
};
render() {
return (
<input
ref={this.ref}
type="text"
value={this.state.content}
onChange={this.setContent}
/>
);
}
}
  • Function component
function AutofocusInput() {
const [content, setContent] = useState("");
const ref = useRef(null);
useEffect(() => {
if (ref && ref.current) {
ref.current.focus();
}
}, []);
return (
<input
ref={ref}
type="text"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
);
}

Vue.js

<template>
<input ref="input" type="text" v-model="content" />
</template>
<script>
export default {
name: "autofocus-input",
data() {
return { content: "" };
},
mounted() {
this.$refs.input.focus();
},
};
</script>

Performance-Optimization

React.js

  • PureComponent
class MyReactComponent extends React.PureComponent {
...
}
  • shouldComponentUpdate
class MyReactComponent extends React.Component {
shouldComponentUpdate(nextProps) {...}
...
}
  • React.memo
export default React.memo(
MyReactComponent,
(prevProps, nextProps) => {
...
}
);
  • useMemo
export default function MyReactComponent() {
return React.useMemo(() => {
return <div>...</div>;
}, []);
}
  • useCallback
function MyItem({ item, handleDelete }) {
const handleClick = useCallback(() => handleDelete(item), [
item,
handleDelete,
]);
return <button onClick={handleClick}>{item.name}</button>;
}

Vue.js

  • v:once
<span v-once>This will never change: {{msg}}</span>
  • functional component

https://vuejs.org/v2/guide/render-function.html#Functional-Components

<template functional>
<h1>Hello {{ name }}</h1>
</template>
<script>
export default {
name: "MyVueComponent",
props: {
name: String,
},
};
</script>
  • keep-alive component

https://vuejs.org/v2/api/#keep-alives

<keep-alive>
<component :is="view"></component>
</keep-alive>

Assets

Next.js

/*
|- public/
|-- my-image.png
*/
function MyImage() {
return <img src="/my-image.png" alt="my image" />;
}

Nuxt.js

  • assets

By default, Nuxt uses vue-loader, file-loader and url-loader for strong assets serving.

<!--
|- assets/
|- image.png
-->
<img src="~/assets/image.png" alt="image" />
  • static

automatically served

<!--
|- static/
|- image.png
-->
<img src="/image.png" alt="image" />

Basic-Routes

Next.js

|- pages/
|- index.js → href="/"
|- blog/index.js → href="/blog"

Nuxt.js

|- pages/
|- index.vue → href="/"
|- blog/index.vue → href="/blog"

Dynamic-Routes

Next.js

|- pages/
|- blog/[slug].js → href="/blog/:slug" (eg. /blog/hello-world)
|- [username]/[option].js → href="/:username/:option" (eg. /foo/settings)
|- post/[...all].js → href="/post/*" (eg. /post/2020/id/title)

Nuxt.js

|- pages/
|- blog/[slug].vue → href="/blog/:slug" (eg. /blog/hello-world)
|- _username/_option.vue → href="/:username/:option" (eg. /foo/settings)

Link

Next.js

import Link from "next/link";function Home() {
return (
<Link href="/">
<a>Home</a>
</Link>
);
}

Nuxt.js

<template>
<nuxt-link to="/">Home page</nuxt-link>
</template>

Fetch-On-Server

Next.js

getInitialProps can only be used in the default export of every page

  • < Next.js 9.3 (class component)
import fetch from "isomorphic-unfetch";export default class Page extends React.Component {
static async getInitialProps(ctx) {
const res = await fetch(`https://.../data`);
const data = await res.json();
return { props: { data } };
}
render() {
// Render data...
}
}
  • < Next.js 9.3 (function component)
import fetch from "isomorphic-unfetch";function Page({ data }) {
// Render data...
}
Page.getInitialProps = async (ctx) => {
const res = await fetch(`https://.../data`);
const data = await res.json();
return { props: { data } };
};
  • >= Next.js 9.3
import fetch from "isomorphic-unfetch";function Page({ data }) {
// Render data...
}
export async function getServerSideProps() {
const res = await fetch(`https://.../data`);
const data = await res.json();
return { props: { data } };
}
export default Page;

Nuxt.js

<template>
<div v-if="$fetchState.error">Something went wrong 😭</div>
<div v-if="$fetchState.pending">Loading...</div>
<div v-else>
<h1>{{ post.title }}</h1>
<pre>{{ post.body }}</pre>
<button @click="$fetch">Refresh</button>
</div>
</template>
<script>
import fetch from "node-fetch";
export default {
data() {
return {
post: {},
};
},
async fetch() {
this.post = await this.$http.$get("xxx");
},
fetchOnServer: true,
};
</script>

Layout

Next.js

./pages/_app.js: automatically apply to all pages

export default function MyApp({ Component, pageProps }) {
return (
<React.Fragment>
<MyHeader />
<Component {...pageProps} />
<MyFooter />
</React.Fragment>
);
}

Nuxt.js

layouts/with-header-footer.vue: create layout

<template>
<div>
<MyHeader />
<nuxt />
<MyFooter />
</div>
</template>

pages/index.vue: apply layout

<template>
<!-- Your template -->
</template>
<script>
export default {
layout: "with-header-footer",
};
</script>

Error-Page

Next.js

pages/_error.js

function Error({ statusCode }) {
return (
<p>
{statusCode
? `An error ${statusCode} occurred on server`
: "An error occurred on client"}
</p>
);
}
Error.getInitialProps = ({ res, err }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { statusCode };
};
export default Error;

Nuxt.js

layouts/error.vue

<template>
<div class="container">
<h1 v-if="error.statusCode === 404">Page not found</h1>
<h1 v-else>An error occurred</h1>
<nuxt-link to="/">Home page</nuxt-link>
</div>
</template>
<script>
export default {
props: ["error"],
layout: "blog", // you can set a custom layout for the error page
};
</script>

Meta-Tag

Next.js

import Head from "next/head";function IndexPage() {
return (
<div>
<Head>
<title>My page title</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<p>Hello world!</p>
</div>
);
}

Nuxt.js

<template>
<h1>{{ title }}</h1>
</template>
<script>
export default {
data() {
return {
title: "Hello World!",
};
},
head() {
return {
title: this.title,
meta: [
// To avoid duplicated meta tags when used in child component, set up an unique identifier with the hid key
{
hid: "description",
name: "description",
content: "My custom description",
},
],
};
},
};
</script>

Context

Next.js

getInitialProps can only be used in the default export of every page

function Page({ data }) {
// Render data...
}
Page.getInitialProps = async (context) => {
const { pathname, query, asPath, req, res, err } = context;
// pathname - Current route. That is the path of the page in /pages
// query - Query string section of URL parsed as an object
// asPath - String of the actual path (including the query) shown in the browser
// req - HTTP request object (server only)
// res - HTTP response object (server only)
// err - Error object if any error is encountered during the rendering
return { props: { project: "next" } };
};

Nuxt.js

export default {
asyncData(context) {
// Universal keys
const {
app,
store,
route,
params,
query,
env,
isDev,
isHMR,
redirect,
error,
} = context;
// Server-side
if (process.server) {
const { req, res, beforeNuxtRender } = context;
}
// Client-side
if (process.client) {
const { from, nuxtState } = context;
}
return { project: "nuxt" };
},
};

CLI

React.js: create-react-app

npx create-react-app react-template

Next.js: create-next-app

npx create-next-app next-template

Vue.js: vue-cli

yarn global add @vue/cli
vue create vue-template

Nuxt.js: create-nuxt-app

npx create-nuxt-app nuxt-template

--

--

Andrew
Vue.js Developers

Math → MBA → PM → Consultant → FE developer → Software engineer → ?