DEV Community 👩‍💻👨‍💻

Cover image for Comparing reactivity models - React vs Vue vs Svelte vs MobX vs Solid vs Redux
Mateo Hrastnik for Lloyds digital

Posted on • Updated on

Comparing reactivity models - React vs Vue vs Svelte vs MobX vs Solid vs Redux

If you're reading this article you're probably already familiar with the concept of reactive programming, but just in case, let me explain what is it and why it's great.

When you're writing code, the commands get executed in a particular order - from top to bottom. So if you write...

let x = 10;
let y = x + 5;
Enter fullscreen mode Exit fullscreen mode

Then y will equal 15, and that's just what we expect, but what happens to y if we then change the value of x to 20? The answer is simple - nothing happens to y, its value will still be 15.

The problem is that the second line of code doesn't say let y be the value of x plus 5. What it instead says is let y be the value of x at the moment of declaration, plus 5. That's because the values of x and y are not reactive. If we are to change the value of x, the value of y doesn't change with it.

let x = 10;
let y = x + 5;
let x = 20;

console.log(y); // 15
Enter fullscreen mode Exit fullscreen mode

So how do we declare the variable y to be the value of x plus 5? That's where reactive programming comes in. Reactive programming is a way of programming that makes it possible solve this problem, but it's just a concept - the actual implementation can vary from library to library.

This article will compare some of the more popular reactivity models in the JS ecosystem - especially the ones found in the UI frameworks and libraries. After all, UI is just a function of state, meaning that UI has to react to changes in state.

In order to compare the different approaches to solving this problem, I'll demonstrate how to create a simple To-do app using different frameworks and libraries. We'll keep the UI as minimal as possible. After all, we are comparing reactivity models, and not UI libraries.

Here's how the end product is gonna look like.

Todo app

1. React

It's 2020 in the world of web development, so you've probably heard of React. It's a fantastic UI library, and, as its name would suggest, React can react to stuff. Namely, it can react to changes in state.

Here's how a basic todo app looks like in React.

import React, { useEffect, useState } from "react";

export default function App() {
  const [todoList, setTodoList] = useState([
    { id: 1, task: "Configure ESLint", completed: false },
    { id: 2, task: "Learn React", completed: true },
    { id: 3, task: "Take ring to Mordor", completed: true },
  ]);

  const completedTodoList = todoList.filter((t) => t.completed === true);
  const notCompletedTodoList = todoList.filter((t) => t.completed === false);

  function createTodo(task) {
    setTodoList([...todoList, { id: Math.random(), task, completed: false }]);
  }

  function removeTodo(todo) {
    setTodoList(todoList.filter((t) => t !== todo));
  }

  function setTodoCompleted(todo, value) {
    const newTodoList = todoList.map((t) => {
      if (t === todo) return { ...t, completed: value };
      return t;
    });
    setTodoList(newTodoList);
  }

  function addTodo() {
    const input = document.querySelector("#new-todo");
    createTodo(input.value);
    input.value = "";
  }

  useEffect(() => {
    console.log(todoList.length);
  }, [todoList]);

  return (
    <div>
      <input id="new-todo" />
      <button onClick={addTodo}>ADD</button>

      <div>
        <b>Todo:</b>
        {notCompletedTodoList.map((todo) => {
          return (
            <div key={todo.id}>
              {todo.task}
              <button onClick={() => setTodoCompleted(todo, true)}>
                Complete
              </button>
            </div>
          );
        })}
      </div>

      <div>
        <b>Done:</b>
        {completedTodoList.map((todo) => {
          return (
            <div key={todo.id}>
              {todo.task}
              <button onClick={() => removeTodo(todo)}>Delete</button>
              <button onClick={() => setTodoCompleted(todo, false)}>
                Restore
              </button>
            </div>
          );
        })}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In React, reactive state is created using the useState hook - it returns the state itself, and a setter function to update the state.
When the setter is called the whole component re-renders - this makes it really simple to declare derived data - we simply declare a variable that uses the reactive state.

In the example above, todoList is a list of todo objects, each having a completed attribute. In order to get all the completed todos we can simply declare a variable and filter the data we need.

const completedTodoList = todoList.filter((t) => t.completed === true);
Enter fullscreen mode Exit fullscreen mode

The state updater function can take the new state directly, or we can use an updater function that receives the state as the argument and returns the new state. We have to be careful not to mutate state so when we have some complex state like an object or an array we have to use some ugly tricks like in the setTodoCompleted function above.

It's possible to run a function whenever some reactive state changes using the useEffect hook. In the example we log the length of the todoList whenever it changes. The first argument to useEffect is the function we want to run, and the second is a list of reactive values to track - whenever one of these values changes the effect will run again.

There's one downside to Reacts reactivity model - the hooks (useState and useEffect) have to always be called in the same order and you can't put them inside an if block. This can be confusing for beginners, but there are lint rules that can help warn you if you accidentally make that mistake.

2. Vue

<template>
  <div>
    <input id="new-todo" />
    <button @click="addTodo">ADD</button>

    <div>
      <b>Todo:</b>
      <div v-for="todo in notCompletedTodoList" :key="todo.id">
        {{ todo.task }}
        <button @click="setTodoCompleted(todo, true)">Complete</button>
      </div>
    </div>

    <div>
      <b>Done:</b>
      <div v-for="todo in completedTodoList" :key="todo.id">
        {{ todo.task }}
        <button @click="removeTodo(todo)">Delete</button>
        <button @click="setTodoCompleted(todo, false)">Restore</button>
      </div>
    </div>
  </div>
</template>

<script>
import { ref, computed, watchEffect } from "vue";

export default {
  setup() {
    const todoList = ref([
      { id: 1, task: "Configure ESLint", completed: false },
      { id: 2, task: "Learn React", completed: true },
      { id: 3, task: "Take ring to Mordor", completed: true },
    ]);

    const completedTodoList = computed(() =>
      todoList.value.filter((t) => t.completed === true)
    );
    const notCompletedTodoList = computed(() =>
      todoList.value.filter((t) => t.completed === false)
    );

    function createTodo(task) {
      todoList.value.push({ id: Math.random(), task, completed: false });
    }

    function removeTodo(todo) {
      todoList.value = todoList.filter((t) => t !== todo);
    }

    function setTodoCompleted(todo, value) {
      todo.completed = value;
    }

    function addTodo() {
      const input = document.querySelector("#new-todo");
      createTodo(input.value);
      input.value = "";
    }

    watchEffect(() => {
      console.log(todoList.value.length);
    });

    return {
      completedTodoList,
      notCompletedTodoList,
      addTodo,
      setTodoCompleted,
      removeTodo,
    };
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode
  • Note: I'm using the new Composition API available in Vue 3.0+ that's still in beta but should be available soon.

In Vue we can declare reactive values using the ref function from the Composition API. It returns a reactive value with a value property that tracks everytime you access it. This is so it can actually react to changes - rerun effects and recompute derived values.

We can declare derived values using the computed function. It takes a function and return the derived value - any reactive value accessed in this function is considered a dependency and if it changes, the derived value is also recomputed.

Updating state is as simple as writing to the .value prop of reactive data. Arrays can be changed directly using push, pop, splice and other array methods.

We can run effects when some data changes using watchEffect - it takes a function that runs whenever a reactive value used inside changes.

3. Svelte

Svelte uses a "radical new approach" to building UI - it's a compiler that generates code and leaves no traces of the framework at runtime.

<script>
    let todoList = [
    { id: 1, task: 'Configure ESLint', completed: false },
    { id: 2, task: 'Learn React', completed: true },
    { id: 3, task: 'Take ring to Mordor', completed: true },
  ];

    $: completedTodoList = todoList.filter(t => t.completed === true);
  $: notCompletedTodoList = todoList.filter(t => t.completed === false);

  function createTodo(task) {
    todoList = [...todoList, { id: Math.random(), task, completed: false }];
  }

  function removeTodo(todo) {
    todoList = todoList.filter(t => t !== todo);
  }

    function setTodoCompleted(todo, value) {
        todo.completed = value;
        todoList = todoList
    }

    function addTodo() {
        const input = document.querySelector('#new-todo');
        createTodo(input.value);
        input.value = '';
    }

    $: console.log(todoList.length);
</script>

<div>
    <input id="new-todo" />
    <button on:click={addTodo}>ADD</button>

    <div>
        <b>Todo:</b>
        {#each notCompletedTodoList as todo (todo.id)}
            <div>
                {todo.task}
                <button on:click={() => setTodoCompleted(todo, true)}>Complete</button>
            </div>
        {/each}
    </div>

    <div>
        <b>Done:</b>
        {#each completedTodoList as todo (todo.id