gurumaxi
gurumaxi

Reputation: 486

How to create reactive classes in svelte

I am migrating my codebase to svelte. My business logic contains a lot of classes, which depend on each other. I created a REPL to visualize my problem: Link.

In this example, the money attribute of a person is only updated correctly in the UI if it is directly changed as a html-inline function (left button). When calling a class method (right button), the state of the class is updated, the UI is not.

I am aware this behaviour is intended, and writables should be used to achieve reactivity outside svelte components. Is it somehow possible to achieve reactivity by using classes like in the REPL? Do I need to replace their attributes with writables in order to fix my REPL? If so, how?

<div id="app">
    {#each teams as team}
        <div>
            {#each team.people as person}
                <li class="person-row">
                    <div>{person.name}</div>
                    <div>{person.money}€</div>
                    <button on:click={() => (person.money += 1)}>Add 1€ </button>
                    <button on:click={() => person.addMoney(1)}>Add 1€ </button>
                </li>
            {/each}
        </div>
    {/each}
</div>

<script>
    class Person {
        constructor(name, money) {
            this.name = name;
            this.money = money;
        }

        addMoney(x) {
            this.money += x;
        }
    }

    class Team {
        constructor(name, people) {
            this.name = name;
            this.people = people;
        }
    }

    const p1 = new Person("Person 1", 1000);
    const p2 = new Person("Person 2", 2);
    const p3 = new Person("Person 3", 60);
    const p4 = new Person("Person 4", 15);
    const t1 = new Team("Team 1", [p1, p2]);
    const t2 = new Team("Team 2", [p3, p4]);

    const teams = [t1, t2];
</script>

<style>
    .person-row {
        display: flex;
        flex-direction: row;
    }

    .person-row * {
        width: 100px;
    }
</style>

Upvotes: 4

Views: 2440

Answers (2)

Bob Fanger
Bob Fanger

Reputation: 29917

After calling a method that mutates an object (or array) add an assignment statement:

person.addMoney(1); person = person

This is the recommended method and allows Svelte to only react to changes inside that person object. (Rewriting the app using raw data and direct mutation might be a better fit for Svelte than migrating classes)

Another other option is to use stores. Currently using the $store notation only works at the top level. (Which is helpful for automatic subscribing and unsubscribing)
Your setup with teams and persons makes it hard to create clean setup with the built-in stores.

But the store contract is flexible and powerful and allows using other reactivity systems, like RxJS, Redux and Vue:

import { readable} from "svelte/store"
import { reactive as vueReactive, watch } from 'vue'

function reactive(obj, options) {
  const ref = vueReactive(obj)
    return readable(ref, (set) => {
      return watch(ref, (prev, next) => {
        set(ref)
    }, options);
  })
}

const teams = reactive([t1, t2]);

{#each $teams as team}

Like shown in this REPL

Using this approach has a performance penalty as the teams store is updated on every addMoney causing a lot more invalidation checks by Svelte.
It's a helpful intermediate step during the migration.

Upvotes: 0

Fabrizio Calderan
Fabrizio Calderan

Reputation: 123418

A possible workaround could be to reassign the property to itself, like so:

<button on:click={() => { person.addMoney(1); person.money = person.money; }}

or if you prefer to avoid an explicit reassignment you could also return the money property from the addMoney() method

addMoney(x) {
  this.money += x;
  return this.money;
}

so the button could be

<button on:click={() => { person.money = person.addMoney(1); }}

Upvotes: 1

Related Questions