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

oahehc

Andrew

Posted on April 19, 2020

React.js / Next.js and Vue.js / Nuxt.js Syntax Comparison Side by Side
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.

GitHub logo oahehc / react-vue-comparison

Comparing the syntax of React.js/Next.js and Vue.js/Nuxt.js side by side

Agenda


Render

React.js

ReactDOM.render(<App />, document.getElementById("root"));
Enter fullscreen mode Exit fullscreen mode

Vue.js

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

Basic-Component

React.js

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

Vue.js

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

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">
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

Event-Binding

React.js

  • Class component
class MyReactComponent extends React.Component {
  save = () => {
    console.log("save");
  };

  render() {
    return <button onClick={this.save}>Save</button>;
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Function component
function MyReactComponent() {
  const save = () => {
    console.log("save");
  };

  return <button onClick={save}>Save</button>;
}
Enter fullscreen mode Exit fullscreen mode

Vue.js

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

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} />
}

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

State

React.js

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

  return <h1>Hello {name}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Vue.js

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

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" />
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Children-and-Slot

React.js

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

...

<MyReactComponent>Hello World</MyReactComponent>
Enter fullscreen mode Exit fullscreen mode

Vue.js

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

...

<MyVueComponent>Hello World</MyVueComponent>
Enter fullscreen mode Exit fullscreen mode

Render-HTML

React.js

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

Vue.js

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

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

List-Rendering

React.js

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

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>;
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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>;
}
Enter fullscreen mode Exit fullscreen mode

Vue.js

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

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>
Enter fullscreen mode Exit fullscreen mode

Vue.js

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

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}
      />
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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)}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Performance-Optimization

React.js

  • PureComponent
class MyReactComponent extends React.PureComponent {
  ...
}
Enter fullscreen mode Exit fullscreen mode
  • shouldComponentUpdate
class MyReactComponent extends React.Component {
  shouldComponentUpdate(nextProps) {...}

  ...
}
Enter fullscreen mode Exit fullscreen mode
  • React.memo
export default React.memo(
  MyReactComponent,
  (prevProps, nextProps) => {
    ...
  }
);
Enter fullscreen mode Exit fullscreen mode
  • useMemo
export default function MyReactComponent() {
  return React.useMemo(() => {
    return <div>...</div>;
  }, []);
}
Enter fullscreen mode Exit fullscreen mode
  • useCallback
function MyItem({ item, handleDelete }) {
  const handleClick = useCallback(() => handleDelete(item), [
    item,
    handleDelete,
  ]);

  return <button onClick={handleClick}>{item.name}</button>;
}
Enter fullscreen mode Exit fullscreen mode

Vue.js

  • v:once
<span v-once>This will never change: {{msg}}</span>
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode
  • keep-alive component

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

<keep-alive>
  <component :is="view"></component>
</keep-alive>
Enter fullscreen mode Exit fullscreen mode

Assets

Next.js

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

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" />
Enter fullscreen mode Exit fullscreen mode
  • static

automatically served

<!--
|- static/
  |- image.png
-->
<img src="/image.png" alt="image" />
Enter fullscreen mode Exit fullscreen mode

Basic-Routes

Next.js

|- pages/
  |- index.js        → href="/"
  |- blog/index.js   → href="/blog"
Enter fullscreen mode Exit fullscreen mode

Nuxt.js

|- pages/
  |- index.vue       → href="/"
  |- blog/index.vue  → href="/blog"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Nuxt.js

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

Link

Next.js

import Link from "next/link";

function Home() {
  return (
    <Link href="/">
      <a>Home</a>
    </Link>
  );
}
Enter fullscreen mode Exit fullscreen mode

Nuxt.js

<template>
  <nuxt-link to="/">Home page</nuxt-link>
</template>
Enter fullscreen mode Exit fullscreen mode

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...
  }
}
Enter fullscreen mode Exit fullscreen mode
  • < 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 } };
};
Enter fullscreen mode Exit fullscreen mode
  • >= 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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Nuxt.js

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

<template>
  <div>
    <MyHeader />
    <nuxt />
    <MyFooter />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

pages/index.vue: apply layout

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

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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" } };
};
Enter fullscreen mode Exit fullscreen mode

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" };
  },
};
Enter fullscreen mode Exit fullscreen mode

CLI

React.js: create-react-app

npx create-react-app react-template
Enter fullscreen mode Exit fullscreen mode

Next.js: create-next-app

npx create-next-app next-template
Enter fullscreen mode Exit fullscreen mode

Vue.js: vue-cli

yarn global add @vue/cli
vue create vue-template
Enter fullscreen mode Exit fullscreen mode

Nuxt.js: create-nuxt-app

npx create-nuxt-app nuxt-template
Enter fullscreen mode Exit fullscreen mode

Reference

💖 💪 🙅 🚩
oahehc
Andrew

Posted on April 19, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related