Search code examples
bindingsvelte

Svelte store with deeply nested objects


I just swithced from React(+Redux) to Svelte and faced bunch of problems which I cannot solve by myself. Main one is binding deeply nested objects

Let's assume we got some data structure similar to @hgl's How to use Svelte store with tree-like nested object?. Due to @rixo's answer I understood how to bind Folder and File components on a single page, when I dislpay all at once and there is no routing. But what if I want to make dedicated page for each folder and access it via some id (let's consider name prop as id as it's unique in given model), for example:

  • .../folder/Important%20work%20stuff
  • .../folder/Goats

UPD Here is the REPL from original question

So I want to bind not the root, but nested object to my page's tags. My thoughts:

  • I can try to make a reactive declaration (NOT calculated prop🤦, even though semantically it is) whithin my page with $ like:

    // ./src/folders/[id]/+page.svelte
    <script lang="ts">
          const urlParams = new URLSearchParams(window.location.search);
          const id = urlParams.get("id");
          import { root } from './stores.js'
          $: thisFolder = $root.find((f) => f.name === id) // root level search
              ?? $root.map((pf) => pf.files?.find((f) => f.name === id)).find((f) => f) // 2nd level search, might continue if needed...
          if (!thisFolder) {
              eror(404, { message: "folder not found", });
          }
      </script>
    
      <div>
          <span>Folder name: </span>
          <input bind:value={thisFolder.name}/> // !!! binding here would not work
      </div>
    

But this will not work, my guess thats due to we do not use here same variable, but use reactive (calculated) one. It will react on changes on store, but will not pass it's changes back to store, pls correct me if I'm wrong. UPD Here is the REPL

  • The other option is to make devived store, in which I will pass id and get appropriate folder, but as far as i know derived stores are not bindable, moreover I got no idea how to pass id parameter to derived store. UPD here is the REPL

  • My third though is to listen for changes in inputs and update objects on:change for example, but I wonder if that's might be done with bindings only (also that's how I would do it in React, so looks not reactive for me)

  • The other approach is to make objects as plain as possible, so with each URL redirect load only corresponding to id folder, but that's also like I would do it in react, and that's also not quite situable for my app

Seems like I saw related implementation for my struggle a week ago here on SO, but cannot find that topic since

UPD 1 Added some REPLs, looks like first approach is correct, will check and update question later

UPD 2 Here's @hackape approach REPL, thats work, but a bit not as I expected. If I change value in one component, other will re-render only after I change any value, and JSON representation does not updates at all. I suppose thats because id passed outside derived, but not sure. Continue to investigate...


Solution

  • Update: I see your $: reactive declaration solution, it works alright. My answer focuses on how to approach the problem using store.

    Store is not superior or anything, you can use whichever approach as you see fit. Using store makes more sense when you need to encapsulate a piece of logic for coding sharing.


    Here's how you can create the derived store that you want.

    <script>
        import { derived } from 'svelte/store';
        import { root } from './stores.js'
        import Folder from "./Folder.svelte";
            
        function findFolder(root, id) {
            let target = root.find(child => child.name === id);
            if (target) return target;
            return root.reduce((found, child) => {
                if (found) return found;
                if (child.type === 'folder') {
                    return findFolder(child.files, id);
                }
                return null;
            }, null)
        }
    
        const specificFolder = id => derived(root, $root => findFolder($root, id))
        const goatsFolder = specificFolder(id);
        const id = "Goats"
    </script>
    
    <span>Goats folder</span>
    <Folder files={$goatsFolder.files} expanded={true} />
    <div>{JSON.stringify($goatsFolder)}</div>
    
    <br />
    <span>Complete store</span>
    <Folder files={$root} expanded={true} />
    <div>{JSON.stringify($root)}</div>
    

    The derived store from "svelte/store" lib cannot be bound to (e.g. <Folder bind:files={$goatsFolder.files}> won't work) because a derived store is by-design a readonly store, it doesn't have a set method attached to it.

    But this shortcoming can be easily monkey-patched to turn it into a writable store that can be bound to, thanks to the fact that svelte store is only an interface contract. It's not implementation dependent. As long as you conform to the contract, you can "fool" the runtime.

    store = {
        subscribe: (subscription: (value: any) => void) => (() => void),
        set?: (value: any) => void
    }
    

    Monkey patching, or even creating your own store implementation are all legit solutions and are encouraged. Read more in the docs about "Store contract". Here's a simply monkey patcher:

    const specificFolder = id => {
        let setter = null;
        const store = derived(root, ($root, _set) => {
            _set(findFolder($root, id));
            setter = _set;
        });
        // monkey patch to add a `set` method.
        store.set = (newValue) => {
            setter(newValue);
            // notify upstream root store
            root.update($root => $root);
        };
        return store
    }