Is it possible to have two writable stores in svelte subscribe to each other?

105 views Asked by At

There's an object where its data matches with another object, yet they have different structures.

For example, consider this scenario:

The 'Team' object holds the team ID as its key. The 'Team' object contains 'name' and 'users' objects as its values. The 'users' object has the user ID as its key, which doesn't overlap with user IDs from other teams.

So, I want to create a new object that has all users.

The 'users' object can be subscribed to by users, and modifying this should reflect changes in the 'Team' object. Conversely, the 'Team' object can be subscribed to by users, and modifying it should reflect changes in the 'users' object.

How can I achieve this?

I attempted to update each object using a 'subscribe' function in JavaScript files, but I ended up stuck in an infinite loop and failed.

Here's an example code and a REPL."

<script>
    import {writable} from "svelte/store";
    
    const teamDict = writable({})
    const userDict = writable({})

    function initTeamDict() {
        teamDict.set({
            1: {
                name: "good team",
                users: {
                    1: "James",
                    2: "Poppy",
                    48: "Hway"
                }
            },
            2: {
                name: "bad team",
                users: {
                    47: "Kelin",
                    35: "Teo",
                    24: "Ferma"
                }
            }
        })
    }

    function initUserDict() {        
        userDict.set(Object.values($teamDict).reduce((acc, team) => ({...acc, ...team[`users`]}), {}))
    }


</script>

<button on:click={initTeamDict}>init team dict</button>
<button on:click={initUserDict}>init user dict</button>

<div> {JSON.stringify($teamDict)}</div>
<div> {JSON.stringify($userDict)}</div>

<button on:click={() => $teamDict[`1`][`users`][`1`] = "top"}>this button should change userDict also </button>
<button on:click={() => $userDict[`1`] = "bottom"}>this button should change teamDict also </button>

REPL

https://svelte.dev/repl/e6570ac9ca464c15967a43c8311dcd4d?version=4.2.8

Edit

With the help of @ghostmodd's answer, I solved it by writing the following code.

I copied the object and modified it because modifying a copied object does not trigger a subscription.

The modified object may not be rendered in the subscription order, so I created a separate view using derived.

<script>
    import {derived, writable} from "svelte/store";
    import {Button} from "flowbite-svelte";

    const teamDict = writable({})
    const userDict = writable({})

    teamDict.subscribe(($$teamDict) => {
        const userDictCopy = $userDict
        for (const key in userDictCopy) {
            delete userDictCopy[key]
        }
        Object.assign(userDictCopy, Object.values($$teamDict).reduce((acc, team) => ({...acc, ...team[`users`]}), {}))
    })
    userDict.subscribe(($$userDict) => {
        const teamDictCopy = $teamDict
        for (const team of Object.values(teamDictCopy)) {
            team[`users`] = {}
        }
        for (const [userId, user] of Object.entries($$userDict)) {
            teamDictCopy[user[`team_id`]][`users`][userId] = user
        }
    })

    const storeView = derived(
        [teamDict, userDict],
        ([$teamDict, $userDict], set) => {
            set({teamDict: $teamDict, userDict: $userDict})
        }
    )

    function initTeamDict() {
        teamDict.set({
            1: {
                name: "good team",
                users: {
                    1: {
                        "name": "James",
                        "team_id": 1
                    },
                    2: {
                        "name": "Poppy",
                        "team_id": 1
                    },
                    48: {
                        "name": "Hway",
                        "team_id": 1
                    }
                }
            },
            2: {
                name: "bad team",
                users: {
                    47: {
                        "name": "Kelin",
                        "team_id": 2
                    },
                    35: {
                        "name": "Teo",
                        "team_id": 2
                    },
                    24: {
                        "name": "Ferma",
                        "team_id": 2
                    }
                }
            }
        })
    }


</script>

<Button on:click={initTeamDict}>init team dict</Button>

<div> {JSON.stringify($storeView.teamDict)}</div>
<div> {JSON.stringify($storeView.userDict)}</div>

<Button on:click={() => $teamDict[`1`][`users`][`1`][`name`] = "top"}>this button should change userDict also </Button>
<Button on:click={() => $userDict[`1`][`name`] = "bottom"}>this button should change teamDict also </Button>

REPL https://svelte.dev/repl/0b89313017234b8d93d94a4a6adf38ba?version=4.2.8

1

There are 1 answers

1
ghostmodd On

Actually, you can achieve that using intermediate derived store. The derived store is similar to React`s useEffect hook. It watches for updates to specified stores and handles them.

So, except for the "Teams" and "Users" stores you should create a derived store which will catch and save updates.

That`s how I did it. Firstly, I created original writable stores with an empty object value.

const teamDict = writable({})
const userDict = writable({})

Then, I made the intermediate derived store which will follow previously created entities.

const intermediateStore = derived(
    [teamDict, userDict],
    ([$teamDict, $userDict], set) => {
        // your handler here
    },
    {
        teamDict: {},
        userDict: {},
    }
)

Pay attention to the arguments. The first argument is a list of observable stores. The second argument is a function which handles updates. The third argument is an initial value of the derived store.

The third step is to save the required data in the derived store. So, I wrote a handler function in this purpose.

        // create a copy of the teamDict store just to make work more comfortable
        const teamDictCopy = {
            ...$teamDict,
        }

        // Check the userDict store for required data. Actually, you can optimize
        // this loop.
        // I use JSON.parse in aim to get rid of meta data in the store object (f. i., set, update)
        for (let userID in JSON.parse(JSON.stringify($userDict))) {
            const teamID = $teamDict[$userDict[userID].team]

            // Check if the specified team exists
            if ($teamDict[teamID]) {
                teamDictCopy[teamID].users[userID] = $userDict[userID]
            } else {
                console.log("Error! This team hasn`t been initialized yet")
                return
            }
        }

        const userDictCopy = Object.values($teamDict).reduce((acc, team) => ({...acc, ...team[`users`]}), {})

        // setting the intermediate derived state`s value
        set({
            teamDict: teamDictCopy,
            userDict: userDictCopy,
        })

Link to REPL: https://svelte.dev/repl/1966610b5c6c47e497918911ac1d8269?version=4.2.8

P.S. sorry for my English. I'm still learning it :3