Понадобилось тут отобразить данные в виде дерева, с возможностью редактировать разные поля, удалять/добавлять строки и т.д. В процессе поиска подходящих компонентов (хотелось найти под material-ui и react) стал пробовать devextreme-reactive. Ньюанс, однако, оказался в том, что devextreme-reactive хочет данные для дерева в виде плоского массива объектов, в каждом из которых указан parent_id «родителя». А GraphQL сервер у меня отдаёт дерево в виде вложенных друг в друга объектов с массивами объектов. Пришлось делать из одного другое — возможно, кому-то пригодится. А может кто-то скажет, что я заморочился не по делу и всё это делается куда проще.

Итак, в ответ на GraphQL запрос (есть тесты, в каждом есть вопросы, для каждого опроса есть несколько вариантов ответов и мы хотим получить всё сразу):

query TestQuery {
  tests {
    id
    title
    questions {
      id
      title
      answers {
        id
        title
      }
    }
  }
}

Получаем от сервера ответ вида:

Заголовок спойлера
{
  "data": {
    "tests": [
      {
        "id": "test_1",
        "title": "Test 1",
        "questions": [
          {
            "id": "question_1",
            "title": "Question 1 (for t1)",
            "answers": [
              {
                "id": "answer_1",
                "title": "Answer 1 (for q1)"
              },
              {
                "id": "answer_2",
                "title": "Answer 2 (for q1)"
              }
            ]
          },
          {
            "id": "question_2",
            "title": "Question 2 (for t1)",
            "answers": [
              {
                "id": "answer_1_2",
                "title": "Answer 1 (for q2)"
              }
            ]
          }
        ]
      },
      {
        "id": "test_2",
        "title": "Test 2",
        "questions": [
          {
            "id": "question_1_2",
            "title": "Question 1 (for t2)",
            "answers": []
          }
        ]
      },
      {
        "id": "test_3",
        "title": "Test 3",
        "questions": []
      }
    ]
  }
}


Для нормализации используем normalizr:

В описании схемы, через processStrategy, добавляем в детей свойства pid со ссылками на родителей. К слову, в свежем normalizr изменился способ описания схем, из-за чего примеры с assignEntity, ArrayOf, define (каких много) — практически неактуальны.

const answerSchema = new schema.Entity('answers',{}, {
        processStrategy: (entity, parent, key) => { return { ...entity, pid: parent.id} }
    }
)

const questionSchema = new schema.Entity('questions',{
    answers:[answerSchema]}, {
        processStrategy: (entity, parent, key) => { return { ...entity, pid: parent.id} }
    },
)

const testSchema = new schema.Entity('tests',{questions:[questionSchema]}, { 
        processStrategy: (entity, parent, key) => { return { ...entity, pid: 0 } }
    }
)

const nRes = normalize(result.data, {tests: [testSchema]})        

Получаем такое:

Заголовок спойлера
{
  "entities": {
    "answers": {
      "answer_1": {
        "id": "answer_1",
        "title": "Answer 1 (for q1)",
        "__typename": "Answer",
        "pid": "question_1"
      },
      "answer_2": {
        "id": "answer_2",
        "title": "Answer 2 (for q1)",
        "__typename": "Answer",
        "pid": "question_1"
      },
      "answer_1_2": {
        "id": "answer_1_2",
        "title": "Answer 1 (for q2)",
        "__typename": "Answer",
        "pid": "question_2"
      }
    },
    "questions": {
      "question_1": {
        "id": "question_1",
        "title": "Question 1 (for t1)",
        "answers": [
          "answer_1",
          "answer_2"
        ],
        "__typename": "Question",
        "pid": "test_1"
      },
      "question_2": {
        "id": "question_2",
        "title": "Question 2 (for t1)",
        "answers": [
          "answer_1_2"
        ],
        "__typename": "Question",
        "pid": "test_1"
      },
      "question_1_2": {
        "id": "question_1_2",
        "title": "Question 1 (for t2)",
        "answers": [
        ],
        "__typename": "Question",
        "pid": "test_2"
      }
    },
    "tests": {
      "test_1": {
        "id": "test_1",
        "title": "Test 1",
        "questions": [
          "question_1",
          "question_2"
        ],
        "__typename": "Test",
        "pid": 0
      },
      "test_2": {
        "id": "test_2",
        "title": "Test 2",
        "questions": [
          "question_1_2"
        ],
        "__typename": "Test",
        "pid": 0
      },
      "test_3": {
        "id": "test_3",
        "title": "Test 3",
        "questions": [
        ],
        "__typename": "Test",
        "pid": 0
      }
    }
  },
  "result": {
    "tests": [
      "test_1",
      "test_2",
      "test_3"
    ]
  }
}


Нас отсюда интересует только .entities

const normalized = { entities: nRes.entities }

К слову, в процессе въезжания в normalizr, зачитавшись issues, обнаружил, что далеко не я один пытаюсь использовать его не вполне по назначению (наверное просто потому, что это чуть ли не единственный подобный инструмент). Много народу жаждет всяких фич, чтобы получить результат в как можно более настраиваемом формате. Но авторы — кремень.

В силу вышесказанного, результат работы normalizr придётся рихтовать при помощи flat (рекурсивно разворачиваем до нужного уровня вложенности):

const flattened = flatten({ entities: nRes.entities }, { maxDepth: 3 })

Получаем следующее:

Заголовок спойлера
{
  "entities.answers.answer_1": {
    "id": "answer_1",
    "title": "Answer 1 (for q1)",
    "__typename": "Answer",
    "pid": "question_1"
  },
  "entities.answers.answer_2": {
    "id": "answer_2",
    "title": "Answer 2 (for q1)",
    "__typename": "Answer",
    "pid": "question_1"
  },
  "entities.answers.answer_1_2": {
    "id": "answer_1_2",
    "title": "Answer 1 (for q2)",
    "__typename": "Answer",
    "pid": "question_2"
  },
  "entities.questions.question_1": {
    "id": "question_1",
    "title": "Question 1 (for t1)",
    "answers": [
      "answer_1",
      "answer_2"
    ],
    "__typename": "Question",
    "pid": "test_1"
  },
  "entities.questions.question_2": {
    "id": "question_2",
    "title": "Question 2 (for t1)",
    "answers": [
      "answer_1_2"
    ],
    "__typename": "Question",
    "pid": "test_1"
  },
  "entities.questions.question_1_2": {
    "id": "question_1_2",
    "title": "Question 1 (for t2)",
    "answers": [
    ],
    "__typename": "Question",
    "pid": "test_2"
  },
  "entities.tests.test_1": {
    "id": "test_1",
    "title": "Test 1",
    "questions": [
      "question_1",
      "question_2"
    ],
    "__typename": "Test",
    "pid": 0
  },
  "entities.tests.test_2": {
    "id": "test_2",
    "title": "Test 2",
    "questions": [
      "question_1_2"
    ],
    "__typename": "Test",
    "pid": 0
  },
  "entities.tests.test_3": {
    "id": "test_3",
    "title": "Test 3",
    "questions": [
      
    ],
    "__typename": "Test",
    "pid": 0
  }
}


Избавляемся от индексов:

Object.keys(flattened).forEach( (key)=> rows.push(flattened[key]) )

Получаем:

Заголовок спойлера
[
  {
    "id": "answer_1",
    "title": "Answer 1 (for q1)",
    "__typename": "Answer",
    "pid": "question_1"
  },
  {
    "id": "answer_2",
    "title": "Answer 2 (for q1)",
    "__typename": "Answer",
    "pid": "question_1"
  },
  {
    "id": "answer_1_2",
    "title": "Answer 1 (for q2)",
    "__typename": "Answer",
    "pid": "question_2"
  },
  {
    "id": "question_1",
    "title": "Question 1 (for t1)",
    "answers": [
      "answer_1",
      "answer_2"
    ],
    "__typename": "Question",
    "pid": "test_1"
  },
  {
    "id": "question_2",
    "title": "Question 2 (for t1)",
    "answers": [
      "answer_1_2"
    ],
    "__typename": "Question",
    "pid": "test_1"
  },
  {
    "id": "question_1_2",
    "title": "Question 1 (for t2)",
    "answers": [
    ],
    "__typename": "Question",
    "pid": "test_2"
  },
  {
    "id": "test_1",
    "title": "Test 1",
    "questions": [
      "question_1",
      "question_2"
    ],
    "__typename": "Test",
    "pid": 0
  },
  {
    "id": "test_2",
    "title": "Test 2",
    "questions": [
      "question_1_2"
    ],
    "__typename": "Test",
    "pid": 0
  },
  {
    "id": "test_3",
    "title": "Test 3",
    "questions": [
    ],
    "__typename": "Test",
    "pid": 0
  }
]


Можно было бы почистить оставшиеся тут вложенные массивы questions, answers, но это уже мелочи — на отображение не влияют. А __typename нужны, чтобы потом при редактировании понимать, с чем имеем дело.

В компоненте результат обрабатывается так, как это показано у них в примере:

...
<CustomTreeData getChildRows={getChildRows} />
...
const getChildRows = (currentRow, rootRows) => {
    const childRows = rootRows.filter(r => r.pid === (currentRow ? currentRow.id : 0));
    const res = childRows.length ? childRows : null
    return res
}
...

Вроде бы альтернативой всему вышеописанному может являться чтение непосредственно содержимого GraphQL store (в клиенте Apollo) — там всё тоже должно быть уже плоским. Но, честно говоря, я не нашёл, как это можно стандартным способом сделать, да и не очень уверен, что формат, в котором там хранятся данные не изменится в новых версиях.

Комментарии (6)


  1. R33GTRVspec
    04.07.2018 10:09

    Спасибо за инфу.
    А почему у них в демке чекбоксы не отмечаются у детей при отметке родителей?
    Это же косяк, в моем понимании, причем серьезный.


    1. frog Автор
      04.07.2018 11:10

      Это там и в других примерах так…
      Могу ошибаться (не использовал отметки) но, по-моему, у них дерево — это нахлобучка сверху на обычный grid. Соответственно, для выборки детей логику нужно самому дописывать, что для примеров посчитали лишним.


    1. Kvet
      04.07.2018 15:00

      Спасибо за фидбек по поводу рекурсивного селекшена. Планировали добавить.


  1. Kvet
    04.07.2018 14:40

    Первый пример на сайте показывает как работать с иерархичными данными: devexpress.github.io/devextreme-reactive/react/grid/docs/guides/tree-data


    1. frog Автор
      04.07.2018 14:52

      Там именно вложенные друг в друга объекты с массивами на вход подаются? К сожалению в примере не посмотреть, что за тестовые данные используются.


      1. Kvet
        04.07.2018 14:59

        Да. Массивы вложенных элементов хранятся в items поле элемента.