React vs. Vue (1)

2020-07-18

Part I - React

This summer I did something strange. I wrote the same web application twice, once in React and once in Vue. It’s not something I usually do or recommend, unless you are really really bored. However, there is a unique benefit to it. It gave me a much better understanding of how these two web frameworks compare in terms of design principles, architecture, learning curve, code reusability and productivity. I started out with React in spring this year. The objective was to rewrite an old PHP timesheet application. I’ve been using this application for a decade and it was in dire need of an overhaul.

React vs Vue

Enter React. There were two things about React that attracted me: its functional nature and JSX. In React, you can create components with all bells and whistles as simple Javascript functions. This is great for two reasons. It gives you the benefits of functional programming and it keeps all program logic together in one place. The second point is obvious: it is easier to understand functionality if it isn’t split up, spread, and cross-referenced in different files, such as templates, JS code, configuration files, etc. The benefits of functional programming have often been discussed. For me, the most important ones are immutability = the avoidance of (unnecessary) state, better reusability and easier testability. Also, functional programming is a great match for componentisation, something which most current web frameworks build on.

JSX is an XML extension of the Javascript language which kind of blurs the boundaries between Javascript and markup. Any experienced web developer will pick it up very fast, because it feels quite natural to write JSX and it is more readable than either ES6 template strings or ES5 string expressions. Despite of this, React is neither easy to learn nor easy to use. In fact, the learning curve for React is considered to be on par with larger and more complex frameworks such as Angular. To become productive, in addition to the core library, you also need to learn the use of additional modules, such as routing and state management libraries.

So what makes learning React tricky? - In my opinion, there are two things responsible for this: state management and lifecycle methods. Although these are not complicated per se, there are a number of different ways they can be implemented. In React, it is not at all clear which method is the best, and this makes things quite confusing. Let’s start with state management, since this is what really sets React apart. The key word and the concept that anyone interested in React needs to understand is one-way data bindings. React enforces this very strictly. State changes are always propagated downwards in the component hierarchy and never upwards. In other words, the state of a component B can be propagated to its children C and D, but not to its parent A. This is called unidirectional data flow.

Unidirectional flow has far-reaching implications on program design. Because an application is conceptually a component tree, unidirectional flow implies that all state which is accessed by more than one component must be “held” and managed by a parent component. For example, the state a form and all of its input fields may be held and managed by a form component. But what if the same state information is also accessed by other parts of the application, for example a print module or a statistics module? This could become difficult to handle with components alone. Fortunately, there is a solution. React solves this problem through a paradigm called “Flux” architecture.

The Flux architecture introduces a central dispatcher and a “store”, i.e. the part which holds state information from different components anywhere in the component hierarchy and manages state transitions. When an action is dispatched, this causes a state change in some part of the store, which in turn triggers the linked components to update and rerender. Sounds complicated? Yes, indeed! The Flux architecture is probably not needed for most simple use cases. As mentioned, state and data flow can be managed by the components themselves. In a simple application that is probably all you need. But once the application becomes more complex, unidirectional data flow becomes unwieldy. This is the point where you have to add some form of state management, for example as implemented by the Redux library.

So, how does React normally handle component state? You cannot modify state information directly through assignments. I did mention that React is “strict”, didn’t I? Instead, React components inherit an asynchronous method called setState() that accepts any data structure. You must call setState() and pass in the new data. React will then rerender the component. This is the only (sanctioned) way to update the DOM in React. But wait a minute. I talked about functional components earlier and there is no inheritance in functional programming, right? Yes, stateful components could previously only be implemented as objects in Javascript, which means you had to write classes.

In React version 16.8, so-called “hooks” were introduced. These are functions imported from the React library that “hook up” a component to React’s state management and lifecycle mechanisms. With these hooks it is possible to create functional components with reactive rendering and state management. The useState() hook, for example, let’s you define state variables and provides access to the stateState() method. The weirdly named useEffects() hook taps into lifecycle events to perform “side effects”, hence the name. It is used to load data, update content, or perform computations after the component has rendered.

Example: self-updating clock component with local state using React hooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useState, useEffect } from 'react'
import Typography from '@material-ui/core/Typography'
import HtmlTooltip from '@material-ui/core/Tooltip'

const Clock = () => {
const [localTimeAndDate, setLocalTimeAndDate] = useState(new Date())

useEffect(() => {
setInterval(() => setLocalTimeAndDate(new Date()), 1000)
}, [])

return (
<HtmlTooltip title={
<React.Fragment>
<Typography color="inherit">{localTimeAndDate.toLocaleDateString()}</Typography>
</React.Fragment>
}>
<span>{localTimeAndDate.toLocaleTimeString()}</span>
</HtmlTooltip>
)
}

export default Clock

The final thing I want to mention about React is the Context API. This is likewise a relatively new feature which was introduced with version 16.3. In a nutshell, the context API gives you a simple state container that manages application-wide state. It lacks the sophistication of the Flux architecture, but it is very useful for managing “global” state, for example access control, an internationalisation feature or any other infrastructure service. The context API does this by offering a Context.Provider component that makes this information available to all descendant components. In addition, there is a Context.Consumer component which accepts code that reacts to context state changes. Together, these two components often eliminate the need for an add-on state management library, such as Redux. Frankly, I don’t understand how many tricks application developers had to employ prior to version 16.3 to manage global state.

Example: i18n feature using the React Context API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React from 'react'
let i18nCatalog = new Map()
import('../i18n/en').then(catalog => {
i18nCatalog = catalog.default
})

const Context = React.createContext({
language: 'en',
changeLanguage: (language: string) => {}
})

export const _ = (key: string) => {
return i18nCatalog.has(key) ? i18nCatalog.get(key) : key
}

export class LanguageProvider extends React.Component {
state = {
language: 'en',
}

changeLanguage = (language: string) => {
import(`../i18n/${language}`).then(catalog => {
i18nCatalog = catalog.default
this.setState({ language })
})
}

render() {
return (
<Context.Provider value={{...this.state, changeLanguage: this.changeLanguage}}>
{this.props.children}
</Context.Provider>
)
}
}

export default Context