Svelte-магазин с глубоко вложенными объектами

Я только что перешёл с React(+Redux) на Svelte и столкнулся с кучей проблем, которые не могу решить самостоятельно. Основной из них — привязка глубоко вложенных объектов.

Предположим, мы получили структуру данных, похожую на структуру @hgl Как использовать хранилище Svelte с древовидным вложенным объектом?. Благодаря ответу @rixo я понял, как связать компоненты «Папка» и «Файл» на одной странице, когда я отображаю все сразу и нет маршрутизации. Но что, если я хочу создать отдельную страницу для каждой папки и получать к ней доступ через какой-нибудь id (давайте считать, что name prop как id, поскольку он уникален в данной модели), например:

  • .../папка/Важное%20работа%20материалы
  • .../папка/Козы

UPD Вот REPL из исходного вопроса

Поэтому я хочу привязать к тегам моей страницы не корневой, а вложенный объект. Мои мысли:

  • Я могу попытаться сделать реактивное объявление (НЕ рассчитанное prop🤦, хотя семантически это так) на моей странице с помощью $, например:

    // ./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>
    

Но это не сработает, я думаю, это связано с тем, что мы не используем здесь ту же переменную, а используем реактивную (расчетную). Он отреагирует на изменения в магазине, но не передаст их обратно в магазин. Пожалуйста, поправьте меня, если я ошибаюсь. UPD Вот REPL

  • Другой вариант — создать хранилище Devived, в которое я передам id и получу соответствующую папку, но, насколько я знаю, производные хранилища не являются привязываемыми, более того, я понятия не имею, как передать параметр id в производное хранилище. UPD вот REPL

  • Моя третья задача — прослушивать изменения во входных данных и обновлять объекты, например, on:change, но мне интересно, можно ли это сделать только с помощью привязок (также именно так я бы сделал это в React, поэтому для меня это выглядит нереактивным)

  • Другой подход состоит в том, чтобы сделать объекты максимально простыми, чтобы каждое перенаправление URL-адреса загружалось только в соответствии с папкой id, но это также похоже на то, как будто я сделал бы это в реакции, и это также не совсем подходит для моего приложения.

Похоже, неделю назад я видел соответствующую реализацию для своей борьбы здесь, на SO, но не могу найти эту тему, так как

UPD 1 Добавлено несколько REPL, похоже, первый подход правильный, позже проверю и обновлю вопрос.

UPD 2 Вот подход @hackape REPL, это работает, но немного не так, как я ожидал. Если я изменю значение в одном компоненте, другой будет повторно отображаться только после того, как я изменю какое-либо значение, а представление JSON вообще не обновится. Полагаю, это потому, что id вышел наружу derived, но не уверен. Продолжайте расследование...

🤔 А знаете ли вы, что...
С Svelte можно создавать прогрессивные веб-приложения (PWA) с поддержкой оффлайн-режима.


1
59
1

Ответ:

Решено

Обновление: я вижу ваше $: реактивное объявление решение, оно работает нормально. Мой ответ посвящен тому, как решить проблему с помощью магазина.

Магазин не является чем-то лучшим или чем-то еще, вы можете использовать любой подход, который считаете нужным. Использование хранилища имеет больше смысла, когда вам нужно инкапсулировать часть логики для совместного использования кода.


Вот как можно создать нужное производное хранилище.

<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>

Хранилище derived из "svelte/store" lib не может быть привязано (например, <Folder bind:files = {$goatsFolder.files}> не будет работать), поскольку производное хранилище по своей конструкции является хранилищем только для чтения, к нему не прикреплен метод set.

Но этот недостаток можно легко исправить, превратив его в доступное для записи хранилище, к которому можно привязаться, поскольку svelte store — это всего лишь контракт интерфейса. Это не зависит от реализации. Пока вы соблюдаете контракт, вы можете «обмануть» среду выполнения.

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

Исправление обезьян или даже создание собственной реализации магазина — все это законные решения и приветствуются. Подробнее читайте в документации о «Договоре магазина». Вот просто обезьяний патчер:

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
}