Home > Software engineering >  Seemingly contradictory reactivity in Svelte assignment to array element
Seemingly contradictory reactivity in Svelte assignment to array element

Time:08-02

I'm new to Svelte and experimenting with a trivial Tic Tac Toe implementation.

I'm confused why reactivity is working when I update the elements of an array, contrary to the documentation which says that its necessary to assign the array to itself to trigger subscription notifications.

I can't setup a REPL for the application as I'm using Typescript, the Svelte REPL seems to only support JS, so I'm hoping that this main Board.svelte file is sufficient, as this seems to be where the issue lies.

The question I have relates to the tiles array, which is updated in a click handler. Note that the click handler is not reactive (no $: and as incline function rather than an arrow function), but modifications to tiles[index] seem to cause reactivity, because the UI updates, and so also does the gameOver variable which is computed whenever the board changes (it returns either undefined if the game is in progress, or an object containing the winning player and the tile indexes that comprise the win).

Note that the isWin function is written with reactivity - if I remove the $: it stops updating the isWin prop on the tile, which controls whether the tile is highlighted on game over.

This seems to be contradictory - gameOver needs tiles passing as an argument and to be marked reactive or it won't react on tile updates (as I would expect). isWin needs to be reactive, or it won't cause Tile to re-render.

The but I can't explain is why click managed to cause updates to be fired, when the array itself is not updating (only the element). NB: Removing references to gameOver and activePlayer don't make any difference to subscription notifications on tiles.

I specifically avoided using svelte/stores in this implementation. I got in a mess trying to use them and stripped it back to basic reactivity and found that did everything I needed, and I'm not clear if the 'array = array' convention is different when using stores vs reactive vars.

I'd really like to understand this better before moving forward.

<script lang="ts">
    import Tile from './Tile.svelte'
    import Result from "./Result.svelte"
    import Reset from "./Reset.svelte"
    import type { GameOver, Player } from "./types"
    import { getGameOver } from "./getGameOver"

    let activePlayer: Player = 'X'
    const tiles: Array<Player | undefined> = Array(9).fill(undefined)

    let gameOver: GameOver | undefined
    $: gameOver = getGameOver(tiles)

    let isWin: (index: number) => boolean
    $: isWin = (index: number) => {
        return gameOver?.winLine?.includes(index) ?? false
    }

    function click(index: number) {
        if (!gameOver && tiles[index] === undefined) {
            tiles[index] = activePlayer
            activePlayer = activePlayer === 'X' ? 'O' : 'X'
        }
    }

    function reset() {
        for (const index in tiles) {
            tiles[index] = undefined
        }
        activePlayer = 'X'
    }

</script>

<div  class:gameOver={!!gameOver}>
    <Result gameOver={gameOver} activePlayer={activePlayer}/>

    <div >
        {#each tiles as tile, i}
            <Tile player={tile} isWin={isWin(i)} click={() => click(i)}/>
        {/each}
    </div>
    <Reset reset={reset}/>
</div>

<style>
    .tiles {
        display: grid;
        grid-template-columns: repeat(3, 100px);
        grid-template-rows: repeat(3, 100px);
    }
</style>

CodePudding user response:

Assigning an element of an array is just like assigning any other object property, so the array will be marked as dirty.

What the documentation is warning about is the use of functions which internally change the array, e.g. push or pop. This would require Svelte to know what the various functions do to keep reactivity.


Svelte analyzes the code to see where modifications of state happen, e.g.

<script>
    let items = ['a', 'b', 'c'];
</script>

<button on:click={() => items[0] = '!'}>
    Change Item 0
</button>

Here the compiled code for the click handler will contain additional logic to invalidate items, because its property '0' has been modified.

function instance($$self, $$props, $$invalidate) {
    let items = ['a', 'b', 'c'];
    const click_handler = () => $$invalidate(0, items[0] = '!', items);
    return [items, click_handler];
}

This happens regardless of whether the object is an array or not, or whether additional nested properties are added.

  • Related