Как бороться с реляционными данными в Redux?

Приложение, которое я создаю, имеет множество сущностей и связей (база данных реляционная). Чтобы понять, существует более 25 сущностей, с любым типом отношений между ними (один ко многим, много ко многим).

Приложение основано на React + Redux. Для получения данных из Магазина мы используем библиотеку Reselect.

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

Чтобы лучше объяснить проблему, я создал простое демонстрационное приложение с похожей архитектурой. Я выделю наиболее важную кодовую базу. В конце я включу фрагмент (скрипку), чтобы поиграть с ним.

Демо приложение

Бизнес логика

У нас есть Книги и Авторы. Одна книга имеет одного автора. У одного автора много книг. Как можно проще.

const authors = [{
  id: 1,
  name: 'Jordan Enev',
  books: [1]
}];

const books = [{
  id: 1,
  name: 'Book 1',
  category: 'Programming',
  authorId: 1
}];

Redux Store

Магазин организован в плоской структуре, в соответствии с лучшими практиками Redux- Normalizing State Shape.

Вот начальное состояние для книжных и авторских магазинов:

const initialState = {
  // Keep entities, by id:
  // { 1: { name: '' } }
  byIds: {},
  // Keep entities ids
  allIds:[]
};

Компоненты

Компоненты организованы в виде контейнеров и презентаций.

<App /> Компонент действует как Контейнер (получает все необходимые данные):

const mapStateToProps = state => ({
  books: getBooksSelector(state),
  authors: getAuthorsSelector(state),
  healthAuthors: getHealthAuthorsSelector(state),
  healthAuthorsWithBooks: getHealthAuthorsWithBooksSelector(state)
});

const mapDispatchToProps = {
  addBooks, addAuthors
}

const App = connect(mapStateToProps, mapDispatchToProps)(View);

<View /> Компонент только для демонстрации. Он отправляет фиктивные данные в хранилище и отображает все компоненты презентации в виде <Author />, <Book />,

Селекторы

Для простых селекторов это выглядит просто:

/**
 * Get Books Store entity
 */
const getBooks = ({books}) => books;

/**
 * Get all Books
 */
const getBooksSelector = createSelector(getBooks,
    (books => books.allIds.map(id => books.byIds[id]) ));


/**
 * Get Authors Store entity
 */
const getAuthors = ({authors}) => authors;

/**
 * Get all Authors
 */
const getAuthorsSelector = createSelector(getAuthors,
    (authors => authors.allIds.map(id => authors.byIds[id]) ));

Это становится грязным, когда у вас есть селектор, который вычисляет / запрашивает реляционные данные. Демо-приложение включает в себя следующие примеры:

  1. Получение всех авторов, у которых есть хотя бы одна книга в определенной категории.
  2. Попадают те же авторы, но вместе со своими книгами.

Вот противные селекторы:

/**
 * Get array of Authors ids,
 * which have books in 'Health' category
 */  
const getHealthAuthorsIdsSelector = createSelector([getAuthors, getBooks],
    (authors, books) => (
    authors.allIds.filter(id => {
      const author = authors.byIds[id];
      const filteredBooks = author.books.filter(id => (
        books.byIds[id].category === 'Health'
      ));

      return filteredBooks.length;
    })
)); 

/**
 * Get array of Authors,
 * which have books in 'Health' category
 */   
const getHealthAuthorsSelector = createSelector([getHealthAuthorsIdsSelector, getAuthors],
    (filteredIds, authors) => (
    filteredIds.map(id => authors.byIds[id])
)); 

/**
 * Get array of Authors, together with their Books,
 * which have books in 'Health' category
 */    
const getHealthAuthorsWithBooksSelector = createSelector([getHealthAuthorsIdsSelector, getAuthors, getBooks],
    (filteredIds, authors, books) => (
    filteredIds.map(id => ({
        ...authors.byIds[id],
      books: authors.byIds[id].books.map(id => books.byIds[id])
    }))
));

Подводя итоги

  1. Как видите, вычисление / запрос реляционных данных в селекторах слишком сложен.
    1. Загрузка детских отношений (Автор-> Книги).
    2. Фильтрация по дочерним объектам (getHealthAuthorsWithBooksSelector()).
  2. Будет слишком много параметров селектора, если у сущности много дочерних отношений. Проверять, выписываться getHealthAuthorsWithBooksSelector() и представьте, есть ли у автора еще больше отношений.

Итак, как вы справляетесь с отношениями в Redux?

Это похоже на общий случай использования, но на удивление нет хороших практик раунда.

* Я проверил библиотеку redux-orm, и она выглядит многообещающе, но ее API все еще нестабилен, и я не уверен, что она готова к работе.

const { Component } = React
const { combineReducers, createStore } = Redux
const { connect, Provider } = ReactRedux
const { createSelector } = Reselect

/**
 * Initial state for Books and Authors stores
 */
const initialState = {
  byIds: {},
  allIds:[]
}

/**
 * Book Action creator and Reducer
 */

const addBooks = payload => ({
  type: 'ADD_BOOKS',
  payload
})

const booksReducer = (state = initialState, action) => {
  switch (action.type) {
  case 'ADD_BOOKS':
    let byIds = {}
    let allIds = []

    action.payload.map(entity => {
      byIds[entity.id] = entity
      allIds.push(entity.id)
    })

    return { byIds, allIds }
  default:
    return state
  }
}

/**
 * Author Action creator and Reducer
 */

const addAuthors = payload => ({
  type: 'ADD_AUTHORS',
  payload
})

const authorsReducer = (state = initialState, action) => {
  switch (action.type) {
  case 'ADD_AUTHORS':
    let byIds = {}
    let allIds = []

    action.payload.map(entity => {
      byIds[entity.id] = entity
      allIds.push(entity.id)
    })

    return { byIds, allIds }
  default:
    return state
  }
}

/**
 * Presentational components
 */
const Book = ({ book }) => <div>{`Name: ${book.name}`}</div>
const Author = ({ author }) => <div>{`Name: ${author.name}`}</div>

/**
 * Container components
 */

class View extends Component {
  componentWillMount () {
    this.addBooks()
    this.addAuthors()
  }

  /**
   * Add dummy Books to the Store
   */
  addBooks () {
    const books = [{
      id: 1,
      name: 'Programming book',
      category: 'Programming',
      authorId: 1
    }, {
      id: 2,
      name: 'Healthy book',
      category: 'Health',
      authorId: 2
    }]

    this.props.addBooks(books)
  }

  /**
   * Add dummy Authors to the Store
   */
  addAuthors () {
    const authors = [{
      id: 1,
      name: 'Jordan Enev',
      books: [1]
    }, {
      id: 2,
      name: 'Nadezhda Serafimova',
      books: [2]
    }]

    this.props.addAuthors(authors)
  }

  renderBooks () {
    const { books } = this.props

    return books.map(book => <div key={book.id}>
      {`Name: ${book.name}`}
    </div>)
  }

  renderAuthors () {
    const { authors } = this.props

    return authors.map(author => <Author author={author} key={author.id} />)
  }

  renderHealthAuthors () {
    const { healthAuthors } = this.props

    return healthAuthors.map(author => <Author author={author} key={author.id} />)
  }

  renderHealthAuthorsWithBooks () {
    const { healthAuthorsWithBooks } = this.props

    return healthAuthorsWithBooks.map(author => <div key={author.id}>
      <Author author={author} />
      Books:
      {author.books.map(book => <Book book={book} key={book.id} />)}
    </div>)
  }

  render () {
    return <div>
      <h1>Books:</h1> {this.renderBooks()}
      <hr />
      <h1>Authors:</h1> {this.renderAuthors()}
      <hr />
      <h2>Health Authors:</h2> {this.renderHealthAuthors()}
      <hr />
      <h2>Health Authors with loaded Books:</h2> {this.renderHealthAuthorsWithBooks()}
    </div>
  }
};

const mapStateToProps = state => ({
  books: getBooksSelector(state),
  authors: getAuthorsSelector(state),
  healthAuthors: getHealthAuthorsSelector(state),
  healthAuthorsWithBooks: getHealthAuthorsWithBooksSelector(state)
})

const mapDispatchToProps = {
  addBooks, addAuthors
}

const App = connect(mapStateToProps, mapDispatchToProps)(View)

/**
 * Books selectors
 */

/**
 * Get Books Store entity
 */
const getBooks = ({ books }) => books

/**
 * Get all Books
 */
const getBooksSelector = createSelector(getBooks,
  books => books.allIds.map(id => books.byIds[id]))

/**
 * Authors selectors
 */

/**
 * Get Authors Store entity
 */
const getAuthors = ({ authors }) => authors

/**
 * Get all Authors
 */
const getAuthorsSelector = createSelector(getAuthors,
  authors => authors.allIds.map(id => authors.byIds[id]))

/**
 * Get array of Authors ids,
 * which have books in 'Health' category
 */
const getHealthAuthorsIdsSelector = createSelector([getAuthors, getBooks],
  (authors, books) => (
    authors.allIds.filter(id => {
      const author = authors.byIds[id]
      const filteredBooks = author.books.filter(id => (
        books.byIds[id].category === 'Health'
      ))

      return filteredBooks.length
    })
  ))

/**
 * Get array of Authors,
 * which have books in 'Health' category
 */
const getHealthAuthorsSelector = createSelector([getHealthAuthorsIdsSelector, getAuthors],
  (filteredIds, authors) => (
    filteredIds.map(id => authors.byIds[id])
  ))

/**
 * Get array of Authors, together with their Books,
 * which have books in 'Health' category
 */
const getHealthAuthorsWithBooksSelector = createSelector([getHealthAuthorsIdsSelector, getAuthors, getBooks],
  (filteredIds, authors, books) => (
    filteredIds.map(id => ({
      ...authors.byIds[id],
      books: authors.byIds[id].books.map(id => books.byIds[id])
    }))
  ))

// Combined Reducer
const reducers = combineReducers({
  books: booksReducer,
  authors: authorsReducer
})

// Store
const store = createStore(reducers)

const render = () => {
  ReactDOM.render(<Provider store={store}>
    <App />
  </Provider>, document.getElementById('root'))
}

render()
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.24/browser.js"></script>
<script src="https://npmcdn.com/reselect@3.0.1/dist/reselect.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.3.1/redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.4.6/react-redux.min.js"></script>

JSFiddle.

4 ответа

Это напоминает мне о том, как я начал один из моих проектов, где данные были очень реляционными. Вы все еще слишком много думаете о бэкэнд-способе выполнения вещей, но вы должны начать думать о большем количестве способов ведения дел JS (страшная мысль для некоторых, чтобы быть уверенным).

1) Нормализованные данные в состоянии

Вы проделали хорошую работу по нормализации ваших данных, но на самом деле, это только несколько нормализовано. Почему я так говорю?

...
books: [1]
...
...
authorId: 1
...

У вас есть одинаковые концептуальные данные, хранящиеся в двух местах. Это может легко стать не синхронизированным. Например, допустим, вы получаете новые книги с сервера. Если они все имеют authorId из 1, вы также должны изменить саму книгу и добавить эти идентификаторы к ней! Это много дополнительной работы, которую не нужно делать. И если это не будет сделано, данные будут не синхронизированы.

Одно общее правило с архитектурой в стиле редукса никогда не хранит (в состоянии) то, что вы можете вычислить. Это включает в себя это отношение, оно легко вычисляется authorId,

2) Денормализованные данные в селекторах

Мы упоминали, что нормализованные данные в государстве не были хорошими. Но денормализация в селекторах это нормально, верно? Ну, это так. Но вопрос в том, нужно ли это? Я сделал то же самое, что вы делаете сейчас, заставив селектор действовать в основном как бэкэнд ORM. "Я просто хочу иметь возможность позвонить author.books и получить все книги!"вы можете подумать. Было бы так просто, чтобы просто иметь возможность пройтись по author.books в вашем компоненте React, и визуализировать каждую книгу, верно?

Но вы действительно хотите нормализовать каждый фрагмент данных в вашем состоянии? Реакту это не нужно. На самом деле, это также увеличит использование вашей памяти. Это почему?

Потому что теперь у вас будет две копии одного и того же author, например:

const authors = [{
  id: 1,
  name: 'Jordan Enev',
  books: [1]
}];

а также

const authors = [{
  id: 1,
  name: 'Jordan Enev',
  books: [{
      id: 1,
      name: 'Book 1',
      category: 'Programming',
      authorId: 1
  }]
}];

Так getHealthAuthorsWithBooksSelector теперь создает новый объект для каждого автора, который не будет === одному в государстве.

Это не плохо Но я бы сказал, что это не идеально. Помимо избыточного (<- ключевое слово) использования памяти, лучше иметь одну единственную авторитетную ссылку на каждую сущность в вашем магазине. В настоящее время для каждого автора существуют две сущности, которые концептуально совпадают, но ваша программа рассматривает их как совершенно разные объекты.

Так что теперь, когда мы смотрим на ваш mapStateToProps:

const mapStateToProps = state => ({
  books: getBooksSelector(state),
  authors: getAuthorsSelector(state),
  healthAuthors: getHealthAuthorsSelector(state),
  healthAuthorsWithBooks: getHealthAuthorsWithBooksSelector(state)
});

В основном вы предоставляете компоненту 3-4 разных копии одних и тех же данных.

Думать о решениях

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

const mapStateToProps = state => ({
  books: getBooksSelector(state),
  authors: getAuthors(state),
});

Ах, единственные данные, которые действительно нужны этому компоненту! books и authors, Используя данные в нем, он может вычислить все, что ему нужно.

Обратите внимание, что я изменил это с getAuthorsSelector чтобы просто getAuthors? Это потому, что все данные, которые нам нужны для вычислений, находятся в books массив, и мы можем просто вытащить авторов id один у нас их!

Помните, что мы пока не беспокоимся об использовании селекторов, давайте просто подумаем о проблеме в простых терминах. Итак, внутри компонента давайте создадим "указатель" книг их автора.

const { books, authors } = this.props;

const healthBooksByAuthor = books.reduce((indexedBooks, book) => {
   if (book.category === 'Health') {
      if (!(book.authorId in indexedBooks)) {
         indexedBooks[book.authorId] = [];
      }
      indexedBooks[book.authorId].push(book);
   }
   return indexedBooks;
}, {});

И как мы это используем?

const healthyAuthorIds = Object.keys(healthBooksByAuthor);

...
healthyAuthorIds.map(authorId => {
    const author = authors.byIds[authorId];

    return (<li>{ author.name }
       <ul>
         { healthBooksByAuthor[authorId].map(book => <li>{ book.name }</li> }
       </ul>
    </li>);
})
...

И т. Д.

Но, но вы упомянули память ранее, поэтому мы не денормализовали вещи с getHealthAuthorsWithBooksSelector, право? Правильный! Но в этом случае мы не занимаем память избыточной информацией. На самом деле, каждый отдельный объект, books и author с, просто ссылки на оригинальные объекты в магазине! Это означает, что единственной новой памятью занимаются сами массивы / объекты контейнеров, а не сами элементы в них.

Я нашел такое решение идеальным для многих случаев использования. Конечно, я не храню его в компоненте, как описано выше, я извлекаю его в функцию многократного использования, которая создает селекторы на основе определенных критериев. Хотя, я признаю, у меня не было проблем с такой же сложностью, как у вас, в том смысле, что вы должны фильтровать определенную сущность через другую сущность. Хлоп! Но все еще выполнимо.

Давайте извлечем нашу функцию индексатора в функцию многократного использования:

const indexList = fieldsBy => list => {
 // so we don't have to create property keys inside the loop
  const indexedBase = fieldsBy.reduce((obj, field) => {
    obj[field] = {};
    return obj;
  }, {});

  return list.reduce(
    (indexedData, item) => {
      fieldsBy.forEach((field) => {
        const value = item[field];

        if (!(value in indexedData[field])) {
          indexedData[field][value] = [];
        }

        indexedData[field][value].push(item);
      });

      return indexedData;
    },
    indexedBase,
  );
};

Теперь это выглядит как чудовище. Но мы должны сделать некоторые части нашего кода сложными, чтобы мы могли сделать намного больше частей чистыми. Чисто как?

const getBooksIndexed = createSelector([getBooksSelector], indexList(['category', 'authorId']));
const getBooksIndexedInCategory = category => createSelector([getBooksIndexed],
    booksIndexedBy => {
        return indexList(['authorId'])(booksIndexedBy.category[category])
    });
    // you can actually abstract this even more!

...
later that day
...

const mapStateToProps = state => ({
  booksIndexedBy: getBooksIndexedInCategory('Health')(state),
  authors: getAuthors(state),
});

...
const { booksIndexedBy, authors } = this.props;
const healthyAuthorIds = Object.keys(booksIndexedBy.authorId);

healthyAuthorIds.map(authorId => {
    const author = authors.byIds[authorId];

    return (<li>{ author.name }
       <ul>
         { healthBooksByAuthor[authorId].map(book => <li>{ book.name }</li> }
       </ul>
    </li>);
})
...

Конечно, это не так легко понять, потому что он опирается, прежде всего, на составление этих функций и селекторов для построения представлений данных вместо их перенормировки.

Дело в том, что мы не собираемся воссоздавать копии состояния с нормализованными данными. Мы пытаемся * создать индексированные представления (читай: ссылки) этого состояния, которые легко усваиваются компонентами.

Индексирование, которое я здесь представил, очень многоразово, но не без определенных проблем (я позволю всем остальным разобраться). Я не ожидаю, что вы будете использовать это, но я ожидаю, что вы узнаете это из этого: вместо того, чтобы пытаться принудить ваши селекторы предоставить вам бэкэнд-подобные ORM-подобные вложенные версии ваших данных, используйте врожденную способность связывать ваши данные, используя инструменты, которые у вас уже есть: идентификаторы и ссылки на объекты.

Эти принципы могут даже применяться к вашим текущим селекторам. Вместо того, чтобы создавать кучу узкоспециализированных селекторов для каждой мыслимой комбинации данных... 1) Создайте функции, которые создают селекторы для вас на основе определенных параметров 2) Создайте функции, которые можно использовать как resultFunc из множества разных селекторов

Индексирование не для всех, я позволю другим предложить другие методы.

Автор вопроса здесь!

Год спустя, сейчас я собираюсь обобщить мой опыт и мысли здесь.

Я искал два возможных подхода к обработке реляционных данных:

1. Индексирование

aaronofleonard, уже дал нам отличный и очень подробный ответ здесь, где его основная концепция заключается в следующем:

Мы не собираемся воссоздавать копии состояния с нормализованными данными. Мы пытаемся * создать индексированные представления (читай: ссылки) этого состояния, которые легко усваиваются компонентами.

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

  1. Передача много-много дел. Пример: одна книга имеет много авторов через BookStore.
  2. Обработка Глубокая фильтрация. Пример: получить все книги из категории "Здоровые", где по крайней мере "Автор" из определенной страны. Теперь просто представьте, есть ли у нас гораздо больше вложенных уровней сущностей.

Конечно, это выполнимо, но, как вы видите, все может стать очень скоро.

Если вам удобно управлять такой сложностью с помощью индексирования, убедитесь, что у вас достаточно времени для создания селекторов и составления утилит индексирования.

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

Поэтому я решил попробовать библиотеку Redux-ORM.

2. Redux-ORM

Небольшой, простой и неизменный ORM для управления реляционными данными в вашем магазине Redux.

Не вдаваясь в подробности, вот как я управлял всеми требованиями, просто используя библиотеку:

// Handing many-to-many case.
const getBooks = createSelector({ Book } => {
  return Books.all().toModelArray()
   .map( book => ({ 
     book: book.ref,
     authors: book.authors.toRefArray()
   })
})

// Handling Deep filtration.
// Keep in mind here you can pass parameters, instead of hardcoding the filtration criteria.
const getFilteredBooks = createSelector({ Book } => {
  return Books.all().toModelArray()
   .filter( book => {
     const authors = book.authors.toModelArray()
     const hasAuthorInCountry = authors.filter(a => a.country.name === 'Bulgaria').length

     return book.category.type === 'Health' && hasAuthorInCountry
   })
   .map( book => ({ 
     book: book.ref,
     authors: book.authors.toRefArray()
   })
})

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

Также используя .ref мы возвращаем ссылку на сущность Store вместо создания новой копии объекта (вы беспокоитесь о памяти).

Таким образом, имея этот тип селекторов, мой поток выглядит следующим образом:

  1. Контейнерные компоненты извлекают данные через API.
  2. Селекторы получают только необходимый кусок данных.
  3. Визуализация компонентов презентации.

Тем не менее, ничто не идеально, как кажется. Redux-ORM очень просто использует реляционные операции, такие как запросы, фильтрация и т. Д. Здорово!

Но когда мы говорим о возможности повторного использования селекторов, их компоновке, расширении и так далее - это довольно сложная и неловкая задача. Это не проблема Redux-ORM, чем reselect сама библиотека и как она работает. Здесь мы обсуждали тему.

Заключение (личное)

Для более простых реляционных проектов я бы попробовал подход индексирования.

В противном случае, я бы придерживался Redux-ORM, как я использовал его в приложении, для которого я задал вопрос. Там у меня более 70 сущностей и я все еще считаю!

Когда вы начинаете "перегружать" ваши селекторы (например, getHealthAuthorsSelector) с другими именованными селекторами (например, getHealthAuthorsWithBooksSelector...) вы можете получить что-то вроде getHealthAuthorsWithBooksWithRelatedBooksSelector и т. д.

Это не устойчиво. Я предлагаю вам придерживаться высокого уровня (т.е. getHealthAuthorsSelector) и использовать механизм, чтобы их книги и связанные с ними книги и т. д. были всегда доступны.

Вы можете использовать TypeScript и повернуть author.books в получатель, или просто работать с функциями удобства, чтобы получить книги из магазина, когда они необходимы. С помощью действия вы можете комбинировать извлечение из хранилища с извлечением из db для непосредственного отображения (возможно) устаревших данных, а Redux/React позаботится о визуальном обновлении после извлечения данных из базы данных.

Я не слышал об этом Reselect, но кажется, что это может быть хорошим способом иметь все виды фильтров в одном месте, чтобы избежать дублирования кода в компонентах.
Как бы они ни были просты, они также легко тестируемы. Тестирование бизнес-логики / доменной логики обычно (очень?) Хорошая идея, особенно если вы сами не являетесь экспертом в предметной области.

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

Есть библиотека, решающая реляционные выборки: ngrx-entity-relationship.

Подобная демонстрация находится здесь, на codeandbox.

В случае с книгами и авторами это будет так:

Следующий код нам нужно определить один раз.

      // first we need proper state selectors, because of custom field names
const bookState = stateKeys(getBooks, 'byIds', 'allIds');
const authorState = stateKeys(getAuthors, 'byIds', 'allIds');

// now let's define root and relationship selector factories
const book = rootEntitySelector(bookState);
const bookAuthor = relatedEntitySelector(
  authorState,
  'authorId',
  'author'
);

// the same for authors
const author = rootEntitySelector(authorState);
const authorBooks = relatedEntitySelector(
  bookState,
  'books', // I would rename it to `booksId`
  'booksEntities', // and would use here `books`
);

Теперь мы можем создавать селекторы и повторно использовать их, если это необходимо.

      // now we can build a selector
const getBooksWithAuthors = rootEntities(
  book(
    bookAuthor(
      authorBooks(), // if we want to go crazy
    ),
  ),
);

// and connect it
const mapStateToProps = state => ({
  books: getBooksWithAuthors(state, [1, 2, 3]), // or a selector for ids
  // ...
});

Результат будет

      this.props.books = [
  {
    id: 1,
    name: 'Book 1',
    category: 'Programming',
    authorId: 1
    author: {
      id: 1,
      name: 'Jordan Enev',
      books: [1],
      booksEntities: [
        {
          id: 1,
          name: 'Book 1',
          category: 'Programming',
          authorId: 1,
        },
      ],
    },
  },
];
Другие вопросы по тегам