Andrew
Posted on April 19, 2020
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.
oahehc / react-vue-comparison
Comparing the syntax of React.js/Next.js and Vue.js/Nuxt.js side by side
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
Reference
- React.js
- Next.js
- Vue.js
- Nuxt.js
- Vue and React Side by Side
- I created the exact same app in React and Vue. Here are the differences
- Few Tips to Optimizing Performance of React Project
- 5 Extremely Easy ways to drastically improve your VueJS app’s speed
- create-react-app
- create-next-app
- vue-cli
- create-nuxt-app
Posted on April 19, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.