Caktus developers working together

Across industries and sectors, we have become accustomed to seeing great rivalries that drive further innovation and competition. In the early days of car manufacturing, we had Ford and Chevy. In the early days of beverage and bottling, we had Coca-Cola and Pepsi. And today in front-end software development, we have Vue and React. As a framework and a library, respectively, both single page applications (SPAs) are tackling Document Object Model (DOM) Manipulation in markedly improved ways from their predecessor JQuery; with the added benefit of offering state management and code-splitting up front. However, as Django developers, we often struggle to decide between these two titans when choosing a front-end for our Django REST Framework (DRF) APIs. In writing this blog post, we hope to alert you to some of the key distinctions between Vue and React, as well as some of their similarities, before you move forward with either for your team.

Distinction 1 - Component Structure

We recently completed a project that made use of DRF and Vue, which you can read about more in this blog. One of the key reasons the team decided to choose Vue 2.x was due to the framework’s apparent structure. To briefly understand what we mean, let’s look at a component that serves as a counter. First, we will see this component implemented in Vue, and then we’ll have a look at it in React.

// src/components/counterComponent.vue

<template>
  <div class=”some-class-container”>
    <h5>{{ count }}</h5>
    <button @click=”handleClick”>{{ btnNameToLowerCase }}</button>
    <do-something-else />
  </div>
</template>

<script>
// say we are in the “views” (pages) directory
import DoSomethingElse from ‘./DoSomethingElse.vue’
export default {
  data()  {
    return {
      count: 0
    }
  },
  components: {
    DoSomethingElse
  },
  props: {
    btnName: {
      Type: String,
      required: true
    }
  },
  computed: {
    btnNameToLowerCase () {
      return btnName.toLowerCase()
    }
  },
  methods: {
    handleClick () {
      return this.count + 1
    }
  },
}
</script>

If we examine the Vue component above, which has the beginnings of a counter, we see that the data method is managing the component’s state. The components method registers the imported components to be used by our component. The props method registers the properties the component will receive from a parent component and determines the allowed type of those properties. The computed method handles simple logic that will be rendered in the template and re-evaluated when dependencies change, and the methods method is where complex functionality is placed. You can learn more by bootstrapping a project using the vue-cli and reading Vue's API section. Now let’s implement the above component using React version 16.8 which introduces hooks. Similarly, you can bootstrap a React project using create-react-app.

// src/components/counterComponent.js

import { useState } from ‘react’
import PropTypes from ‘prop-types’

// say we are in the “pages” (views) directory
import DoSomethingElse from ‘./DoSomethingElse’
export default function Counter ({ btnName }) {
  const [count, setCount] = useState(0)
  handleClick = () => setCount(count + 1)
  return (
    <div class=”some-class-container”>
      <h5>{ count }</h5>
      <button onClick=”handleClick”>{ btnName.toLowerCase() }</button>
      <DoSomethingElse />
    </div>
  )
}

Counter.propTypes = {
  btnName: PropTypes.string.isRequired
}

Looking at the above, we see that our counter component was implemented in significantly reduced lines of code. This is due in part to the power of hooks and because we have written our component as a function. The useState hook, similar to Vue’s data method, manages the component’s state. Our event handler, onClick, which is similar to the @click directive, triggers our handleClick function that is responsible for updating the state of count using the setCount() function. Props is being provided as a parameter to our functional component (and the type of prop we are expecting is defined using the prop-types npm package. In both components, we expect btnName to be a string, and when it’s rendered to the DOM, our javascript expression (in this case: .toLowerCase()) converts this string to lowercase. In Vue, this javascript expression is placed in a computed property and the computed property’s name is referenced in the template. In React, we can use JSX within the return statement to accomplish the same ends.

Keeping all of this in mind, think about your team. React is a library, and as such it offers great flexibility when laying out components. It also comes with minimal abstraction which gives the developer a lot of freedom to be inventive. Yet, that freedom comes with a cost. As components become more complex it becomes harder to discern at a glance exactly “what does what” without proper planning and foresight. For example, in your component’s return statement you could have an Immediately Invoked Function Expression or IIFE that has conditional logic that returns JSX. Advanced React users may refactor this component and place the logic being supported by the IIFE into a custom hook and use presentational components to render the conditional JSX; but new React programmers may not necessarily see this opportunity, leading to less readable and harder to maintain code.

Keeping design choices of new front-end developers in mind, Vue’s best practices are more explicit and straight forward. This gives product managers and lead developers more flexibility to augment their team with developers of a variety of backgrounds and skill sets without worrying about the initial cost of getting these developers acclimated to the front-end. With Vue, you are trading off granularity with regularity. This could be important if you plan on building a front-end with developers that do not have a lot of SPA experience and you are operating on fixed deadlines. However, if the user interface (UI) you are building is complex, and your team features members who have working knowledge of React design patterns, then you may want to invest in getting the rest of the team up-to-speed on the latest React. With the introduction of hooks, the reusability of stateful logic throughout an application, and the movement away from component lifecycle methods, developers are free to build apps quickly using fewer lines of code and under fewer constraints. Having discussed component structure, let’s take a look at state management in Vue and React.

Distinction 2 - Approaches To State Management

As applications grow in size, it becomes necessary to find a way to manage state across an entire application, or across several components. Both Vue and React offer solutions for how to do so, though in different ways.

The Vue documentation recommends using Vuex to manage state in a store, which is defined in a different part of the application from the components. The store allows data across the app to be retrieved or updated at any time from any component. Following the counter example from above, our Vue application has the following structure (note: not all files required to run a Vue app are mentioned here):

-package.json
-vue.config.js
-src
  components
  counterComponent.vue
  App.js // The top-level component that includes all other components
  main.js
  store.js

Let’s say we wanted to display messages to the user, and we want those messages to be visible to the user regardless of which component the user is on. One way to do this would be to keep the messages in the store, and display them whenever they become available. At this point, our store.js file looks like this:

// store.js

export default {
  state: {
    messages = []
  },
  mutations: {
    setMessages (state, messages) {
      state.messages = messages
    }
  },
  actions: {}
}

We define messages as something that we’re going to keep in the store as a global state variable, and we define a mutation for any time we want to set the state of messages in the store. For instance, if the user’s action raises an error or gets a success while using our app, we can call this.$store.commit('messages', someNewMessages) from any component, and set the messages attribute in the store. To display the messages to the user, we could do something like this in our App.vue (the top-level component of our app):

// App.vue

<template>
  <div v-for=”message in messages”>
    <div>{{ message }}</div>
  </div>
  … other components and things here ...
</template>
<script>
export default {
  computed: {
    messages () {
      return this.$store.state.messages
    },
  }
}
</script>

In the App.vue, we set a messages computed property to the value of the messages in the store, and display them to the user. Therefore, our messages become visible across our app, and any component can set them depending on the user’s behavior, by putting this.$store.commit('messages', someNewMessages) in the relevant place in the code. For example, if a user tries to save some form, we could define a validation method in that component’s methods, to show a message to the user, telling the user that a field in the form is required:

// some component in the app

<template>
  ...
</template>
<script>
export default {
  methods: {
    validateForm () {
      // If the someField is empty, tell the user that it is required
      if (this.someField === '') {
        const message = this.someField + ' is required.'
        `this.$store.commit('messages', [message])`
      }
    }
  }
}

Vue provides a nice structure to define the state: the store, the mutations to change the store, and also actions (asynchronous methods that may also change the store), and Vue separates this logic from components. Developers may find it easy to determine what the store contains, and all of the synchronous and asynchronous calls by looking in one file (or in the store directory, if you choose to break up your store into modules, and import each of them in a store.js).

React offers us the ability to accomplish the same thing, though with less opinion about code structure. We assume here that we are using React16.8 or later, in order to take advantage of hooks. (Note: this React example is based on an exercise from a recent session we did with Cassidy Williams from React Training.) You may instead use a library like Redux to manage your state, though this is much less appealing with the introduction of hooks. From the example with a counter above, your React app structure may look like:

-package.json
-src
  components
  counterComponent.jsx
  App.js
  index.js

If we want to display messages to the user that are visible across the app, we can do so by using React’s Context API, which allows accessing data in different components, without having to pass the data as a prop. Not having to pass the data as a prop is particularly helpful in case a component needs to access some data that its many parents don’t need, and we don’t want to pass that data through the many parents. Next, we can implement either the useState hook or the useReducer hook. Since useReducer is recommended for more complex applications, we’ll show that here. To begin, let’s add a file with a reducer:

// appReducer.js

const initialState = { messages: [] }

const appReducer = (state, action) => {
  switch (action.name) {
    // If the user is setting the messages, then do so from
    // the action's 'messages' attribute.
    case "setMessages": {
      return { ...state, messages: action.messages }
    }
    default: {
      return state
    }
}

export { initialState }
export default appReducer

So far, we only define an initial state (messages are []), and a reducer that uses a switch statement that sets the state’s messages when the action’s name is 'setMessages'.

Next, we can add a file called something like appState.js, in order to allow developers of our app to use reducers throughout the app:

// appState.js

import React, { createContext, useReducer, useContext } from "react"

const AppContext = createContext()

export function AppReducerProvider({ reducer, initialState = {}, children }) {
  const [value, dispatch] = useReducer(reducer, initialState)
  return <AppContext.Provider value={value} children={children} />
}

export function useAppState() {
  /* Use the Context declared in this file. */
  return useContext(Context)
}

We can also wrap our app in the AppReducerProvider, like so, in App.js:

// App.js

import SomeComponent from "app/SomeComponent"
import { AppStateProvider } from "app/appState"
import appReducer, { initialState } from "app/appReducer"

function App() {
  return <div><SomeComponent /></div>
}

export default () => (
  <AppReducerProvider reducer={appReducer} initialState={initialState}>
    <App />
  </AppReducerProvider>
)

Now, we can set the messages from any component by using useAppState() in the component and also calling the dispatch() function like so: dispatch({ name: 'setMessages', messages: [theMessages] }). For example, we may have a component with a form, we can write the following for when the user submits the form:

// components/someComponent.jsx

import { useAppState } from "app/appState"

… other stuff here …

export default function SomeComponent() {
  // Get the current state, and the dispatch() function to set state
  const [{ messages }, dispatch] = useAppState()

  function userIsSubmittingForm () {
    /* The user is submitting the form. If it is valid, then tell the user that
     * is is being submitted. If not, show the user the errors.
     */
    // Get any errors for the form
    const formErrors = ...

    if (!formErrors) {
      dispatch({ name: 'setMessages', messages: ['Submitting form...'] })
      // Submit the form somewhere
      ...
    } else {
      dispatch({ name: 'setMessages', messages: [formErrors] })
    }
  }
}

For reference, the app structure would now look like:

-package.json
-vue.config.js
-src
  components
  counterComponent.jsx
  someComponent.jsx
  App.js
  appState.js
  appReducer.js
  index.js

As you can see, React is able to handle setting the state across the app, by using reducers. Since this approach offers less structure across the app than Vue’s approach, it gives the developer more freedom (and responsibility) in terms of how to structure the code. While this freedom is neither good nor bad, React leaves it up to the developer to structure state in a way that is readable and understandable to other developers (as well as to the current developers when returning to this code in the future).

Similarities Between Vue and React

We hope that the above juxtaposition of component structure and state management in Vue and React will help inform your thinking as to which framework or library is best for your team or project. However, it's important to note that while we are stressing the difference between Vue and React, there is much in common between the two. Both projects are modern, well maintained, and component-based UIs that utilize a virtual DOM to make DOM manipulations. Both projects render inside of a single set of <div> tags, giving developers the freedom to use Vue and React on portions of a server rendered page, or as an all encompassing SPA.

It is also important to note that both communities are well aware of each other. Vue-hooks are currently in an experimental phase and may make an appearance in Vue3. This means the Vue community is actively working on its own implementation of making component state composable and moving the framework forward in a more functional direction like React.

Final Thoughts

To conclude, as developers, learning Vue or React will be beneficial to learning the other because of the many concepts they share and the inspiration each project derives from the other. However, when it comes to deciding on Vue or React for a project, make sure to consider your team’s front-end preferences, especially when it comes to design patterns, state management, and component structure. These areas of concern will have a major impact on the maintainability of the project, as well as your team’s level of derived enjoyment.

New Call-to-action
blog comments powered by Disqus
Times
Check

Success!

Times

You're already subscribed

Times