如何处理Redux中的关系数据? [英] How to deal with relational data in Redux?

查看:122
本文介绍了如何处理Redux中的关系数据?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在创建的应用程序有很多实体和关系(数据库是关系型的)。为了得到一个想法,有25个以上的实体,它们之间有任何类型的关系(一对多,多对多)。



该应用程序是基于React + Redux的。要从商店获取数据,我们使用重新选择库。



我面临的问题是当我尝试从商店获得一个与其关系的实体时。



In为了更好地解释问题,我创建了一个简单的演示应用程序,它具有类似的架构。我将重点介绍最重要的代码库。最后,我将包含一个片段(小提琴),以便与它一起玩。



演示应用



业务逻辑



我们有书籍和作者。一本书有一位作者。一位作者有很多书。尽可能简单。

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

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



Redux商店



商店在扁平结构,符合Redux最佳实践 - 规范状态形状



以下是图书和作者商店的初始状态:

  const initialState = {
//保留实体,按id:
// {1:{name:''}}
byIds:{},
//保持实体ID
allIds:[]
};



组件



组件组织为容器和演示文稿。



< App /> 组件充当Container(获取所有需要的数据):

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

<查看/> 组件是只是为了演示。它将虚拟数据推送到商店,并将所有Presentation组件呈现为< Author />,< Book />



选择器



对于简单的选择器,它看起来很简单:

  / ** 
*获取书店实体
* /
const getBooks =({books})=>图书;

/ **
*获取所有书籍
* /
const getBooksSelector = createSelector(getBooks,
(books => books.allIds.map (id => books.byIds [id])));


/ **
*获取作者商店实体
* /
const getAuthors =({authors})=>作者;

/ **
*获取所有作者
* /
const getAuthorsSelector = createSelector(getAuthors,
(authors => authors.allIds.map (id => authors.byIds [id])));

当你有一个选择器来计算/查询关系数据时,它会变得混乱。
演示应用程序包括以下示例:


  1. 获取所有作者,其中至少有一本特定类别的图书。

  2. 获得相同的作者,但连同他们的书籍。

以下是令人讨厌的选择:

  / ** 
*获取作者ID数组,
*其中有健康类别的书籍
* /
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'
));

返回filteredBooks.length;
})
));

/ **
*获取作者数组,
*其中有健康类别的书籍
* /
const getHealthAuthorsSelector = createSelector([ getHealthAuthorsIdsSelector,getAuthors],
(filteredIds,authors)=>(
filteredIds.map(id => authors.byIds [id])
));

/ **
*获取一系列作者及其书籍,
*,其中包含健康类别的书籍
* /
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} = Reactconst {combineReducers,createStore} = Reduxconst {connect,Provider} = ReactReduxconst {createSelector} = Reselect / ** * Initial book和Authors store的状态* / const initialState = {byIds:{},allIds:[]} / ** * Book Action creator和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}} / ** *作者动作创建者和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}} / ** *演示组件* / const Book =({book})=> < div> {`Name:$ {book.name}`}< / div> const Author =({author})=> < div> {`名称:$ {author.name}`}< / div> / ** *容器组件* /类视图扩展组件{componentWillMount(){this.addBooks()this.addAuthors()} / ** *将虚拟书籍添加到商店* / addBooks(){const books = [{id:1,name:'Programming book',category:'Programming',authorId:1},{id:2,name:'健康书籍,类别:'健康',authorId:2}] this.props.addBooks(books)} / ** *将虚拟作者添加到商店* / addAuthors(){const authors = [{id:1,name :'Jordan Enev',书籍:[1]},{id:2,名称:'Nadezhda Serafimova',书籍:[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返回authors.map(author =>< Author author = {author} key = {author.id} />)} renderHealthAuthors(){const {healthAuthors} = this.props return healthAuthors.map(author => <作者author = {author} key = {author.id} />)} renderHealthAuthorsWithBooks(){const {healthAuthorsWithBooks} = this.props return healthAuthorsWithBooks.map(author =>< div key = {author.id }><作者作者= {作者} />图书:{author.books.map(book =>< Book book = {book} key = {book.id} />)}< / div>)} render(){return< div> < H1>图书:其中/ H1> {this.renderBooks()}< hr /> < H1>作者:其中/ H1> {this.renderAuthors()}< hr /> < h2>健康作者:< / h2> {this.renderHealthAuthors()}< hr /> < h2>加载书籍的健康作者:< / 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 )/ ** *图书选择器* // ** *获取图书商店实体* / const getBooks =({books})=> books / ** *获取所有书籍* / const getBooksSelector = createSelector(getBooks,books => books.allIds.map(id => books.byIds [id]))/ ** *作者选择器* // ** *获取作者商店实体* / const getAuthors =({authors})=> authors / ** *获取所有作者* / const getAuthorsSelector = createSelector(getAuthors,authors => authors.allIds.map(id => authors.byIds [id]))/ ** *获取作者ID数组,*其中有健康类别的书籍* / 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})))/ ** *获取作者数组,* 健康类别中的书籍* / const getHealthAuthorsSelector = createSelector([getHealthAuthorsIdsSelector,getAuthors],(filteredIds,authors)=>(filteredIds.map(id => authors.byIds [id])))/ ** *获取作者数组及其书籍*,其中有健康类别的书籍* / const getHealthAuthorsWithBooksSelector = createSelector([getHealthAuthorsIdsSelector,getAutho] rs,getBooks],(filteredIds,authors,books)=> (filteredIds.map(id =>({... authors.byIds [id],books:authors.byIds [id] .books.map(id => books.byIds [id])})))) // Combined Reducerconst reducers = combineReducers({books:booksReducer,authors:authorsReducer})// Storeconst store = createStore(redurs)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/库/反应-终极版/ 4.4.6 /反应-redux.min.js>< /脚本>  



JSFiddle

解决方案

这让我想起了我是如何开始我的一个数据高度关系的项目。你对后端的做事方式还有太多考虑,但是你必须开始考虑更多的JS做事方式(对于某些人来说这是一个可怕的想法,当然)。



1)状态中的规范化数据



您已经很好地规范了数据,但实际上,它只是稍微规范化了。我为什么这么说?

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

你将相同的概念数据存储在两个地方。这很容易变得不同步。例如,假设您从服务器收到新书。如果他们都有 authorId 为1,你还必须修改本书并将这些ID添加到它!这是很多额外的工作,不需要做。如果没有完成,数据将不同步。



使用redux样式架构的一般经验法则从不存储(在你可以计算什么。这包括这种关系,它可以通过 authorId 轻松计算。



2)选择器中的非规范化数据



我们提到过该州的规范化数据并不好。但是在选择器中对它进行非规范化是对的吗?嗯,确实如此。但问题是,是否需要它?我做了你现在做的同样的事情,让选择器基本上像后端ORM一样。 我只是想打电话给 author.books 并获得所有书籍!你可能在想。只需能够在你的React组件中循环遍历 author.books 并呈现每本书,对吗?



但是,您真的想要规范您所在州的每一项数据吗? React不需要。实际上,它也会增加你的内存使用量。为什么?



因为现在您将拥有相同作者的两个副本,例如:

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

  const authors = [{
id:1,
name:'Jordan Enev',
books:[{
id:1,
name: '第1册',
类别:'编程',
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),
});

啊,这个组件真正需要的唯一数据! 图书作者。使用其中的数据,它可以计算它需要的任何东西。



请注意,我将它从 getAuthorsSelector 改为 getAuthors ?这是因为我们计算所需的所有数据都在 books 数组中,我们可以通过 id 我们有一个!



请记住,我们并不担心使用选择器,让我们用简单的方式来思考问题。所以,内部组件,让我们通过作者构建书籍的索引。

  const {books,authors} = this.props; 

const healthBooksByAuthor = books.reduce((indexedBooks,book)=> {
if(book.category ==='Health'){
if(!(book) indexedBooks中的.authorId)){
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>);
})
...

Etc等。



但是你之前提到了记忆,这就是为什么我们没有使用 getHealthAuthorsWithBooksSelector 对内容进行非规范化,对吧?
正确!但在这种情况下,我们不会占用内存冗余实际上,每个单独的实体, books 作者,只是对原始对象的引用。商店!这意味着唯一占用的新内存是容器数组/对象本身,而不是它们中的实际项目。



我发现这个一种理想的解决方案对于许多用例。当然,我没有将它保存在上面的组件中,我将其提取为可重用的函数,该函数根据特定条件创建选择器。
虽然,我承认我没有遇到与您相同的复杂问题,因为您必须过滤特定实体,通过另一个实体。哎呀!但仍然可行。



让我们将索引器函数提取到一个可重用的函数中:

  const indexList = fieldsBy => list => {
//所以我们不必在循环中创建属性键
const indexedBase = fieldsBy.reduce((obj,field)=> {
obj [field] = { };
返回obj;
},{});

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

if(!(indexedData [field]中的值)){
indexedData [field] [value] = [];
}

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

返回indexedData;
},
indexedBase,
) ;
};

现在这看起来像是一种怪物。但是我们必须使代码的某些部分变得复杂,因此我们可以使更多的部分更加清晰。干净如何?

  const getBooksIndexed = createSelector([getBooksSelector],indexList(['category','authorId'])); 
const getBooksIndexedInCategory = category => createSelector([getBooksIndexed],
booksIndexedBy => {
return indexList(['authorId'])(booksIndexedBy.category [category])
});
//您实际上可以抽象出更多!

...
当天晚些时候
...

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>);
})
...

当然,这并不容易理解,因为它主要依靠组合这些函数和选择器来构建数据表示,而不是重新规范化。



重点是:我们不打算用规范化的数据重新创建状态的副本。我们试图*创建该状态的索引表示(读取:引用),这些表示很容易被组件消化。



我在这里提供的索引是非常可重用的,但不是没有某些问题(我会让其他人都知道这些)。我不指望你使用它,但我希望你学习这个来自:r除了试图强迫你的选择器给你类似后端的,类似ORM的数据嵌套版本之外,还要使用你现有工具链接数据的固有能力:ids和对象引用。



这些原则甚至可以应用于您当前的选择器。而不是为每个可想到的数据组合创建一堆高度专业化的选择器...
1)创建基于某些参数为您创建选择器的函数
2)创建可用作<的函数许多不同选择器的code> resultFunc



索引不适合所有人,我会让其他人建议其他方法。


The app I'm creating has a lot of entities and relationships (database is relational). To get an idea, there're 25+ entities, with any type of relations between them (one-to-many, many-to-many).

The app is React + Redux based. For getting data from the Store, we're using Reselect library.

The problem I'm facing is when I try to get an entity with its relations from the Store.

In order to explain the problem better, I've created a simple demo app, that has similar architecture. I'll highlight the most important code base. In the end I'll include a snippet (fiddle) in order to play with it.

Demo app

Business logic

We have Books and Authors. One Book has one Author. One Author has many Books. As simple as possible.

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

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

Redux Store

Store is organized in flat structure, compliant with Redux best practices - Normalizing State Shape.

Here is the initial state for both Books and Authors Stores:

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

Components

The components are organized as Containers and Presentations.

<App /> component act as Container (gets all needed data):

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 /> component is just for the demo. It pushes dummy data to the Store and renders all Presentation components as <Author />, <Book />.

Selectors

For the simple selectors, it looks straightforward:

/**
 * 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]) ));

It gets messy, when you have a selector, that computes / queries relational data. The demo app includes the following examples:

  1. Getting all Authors, which have at least one Book in specific category.
  2. Getting the same Authors, but together with their Books.

Here are the nasty selectors:

/**
 * 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])
    }))
));


Summing up

  1. As you can see, computing / querying relational data in selectors gets too complicated.

    1. Loading child relations (Author->Books).
    2. Filtering by child entities (getHealthAuthorsWithBooksSelector()).

  2. There will be too many selector parameters, if an entity has a lot of child relations. Checkout getHealthAuthorsWithBooksSelector() and imagine if the Author has a lot of more relations.

So how do you deal with relations in Redux?

It looks like a common use case, but surprisingly there aren't any good practices round.

*I checked redux-orm library and it looks promising, but its API is still unstable and I'm not sure is it production ready.

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.

解决方案

This reminds me of how I started one of my projects where the data was highly relational. You think too much still about the backend way of doing things, but you gotta start thinking of more of the JS way of doing things (a scary thought for some, to be sure).

1) Normalized Data in State

You've done a good job of normalizing your data, but really, it's only somewhat normalized. Why do I say that?

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

You have the same conceptual data stored in two places. This can easily become out of sync. For example, let's say you receive new books from the server. If they all have authorId of 1, you also have to modify the book itself and add those ids to it! That's a lot of extra work that doesn't need to be done. And if it isn't done, the data will be out of sync.

One general rule of thumb with a redux style architecture is never store (in the state) what you can compute. That includes this relation, it is easily computed by authorId.

2) Denormalized Data in Selectors

We mentioned having normalized data in the state was not good. But denormalizing it in selectors is ok right? Well, it is. But the question is, is it needed? I did the same thing you are doing now, getting the selector to basically act like a backend ORM. "I just want to be able to call author.books and get all the books!" you may be thinking. It would be so easy to just be able to loop through author.books in your React component, and render each book, right?

But, do you really want to normalize every piece of data in your state? React doesn't need that. In fact, it will also increase your memory usage. Why is that?

Because now you will have two copies of the same author, for instance:

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

and

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

So getHealthAuthorsWithBooksSelector now creates a new object for each author, which will not be === to the one in the state.

This is not bad. But I would say it's not ideal. On top of the redundant (<- keyword) memory usage, it's better to have one single authoritative reference to each entity in your store. Right now, there are two entities for each author that are the same conceptually, but your program views them as totally different objects.

So now when we look at your mapStateToProps:

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

You are basically providing the component with 3-4 different copies of all the same data.

Thinking About Solutions

First, before we get to making new selectors and make it all fast and fancy, let's just make up a naive solution.

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

Ahh, the only data this component really needs! The books, and the authors. Using the data therein, it can compute anything it needs.

Notice that I changed it from getAuthorsSelector to just getAuthors? This is because all the data we need for computing is in the books array, and we can just pull the authors by id one we have them!

Remember, we're not worrying about using selectors yet, let's just think about the problem in simple terms. So, inside the component, let's build an "index" of books by their author.

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;
}, {});

And how do we use it?

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>);
})
...

Etc etc.

But but but you mentioned memory earlier, that's why we didn't denormalize stuff with getHealthAuthorsWithBooksSelector, right? Correct! But in this case we aren't taking up memory with redundant information. In fact, every single entity, the books and the authors, are just reference to the original objects in the store! This means that the only new memory being taken up is by the container arrays/objects themselves, not by the actual items in them.

I've found this kind of solution ideal for many use cases. Of course, I don't keep it in the component like above, I extract it into a reusable function which creates selectors based on certain criteria. Although, I'll admit I haven't had a problem with the same complexity as yours, in that you have to filter a specific entity, through another entity. Yikes! But still doable.

Let's extract our indexer function into a reusable function:

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,
  );
};

Now this looks like kind of a monstrosity. But we must make certain parts of our code complex, so we can make many more parts clean. Clean how?

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>);
})
...

This is not as easy to understand of course, because it relies primarily on composing these functions and selectors to build representations of data, instead of renormalizing it.

The point is: We're not looking to recreate copies of the state with normalized data. We're trying to *create indexed representations (read: references) of that state which are easily digested by components.

The indexing I've presented here is very reusable, but not without certain problems (I'll let everyone else figure those out). I don't expect you to use it, but I do expect you to learn this from it: rather than trying to coerce your selectors to give you backend-like, ORM-like nested versions of your data, use the inherent ability to link your data using the tools you already have: ids and object references.

These principles can even be applied to your current selectors. Rather than create a bunch of highly specialized selectors for every conceivable combination of data... 1) Create functions that create selectors for you based on certain parameters 2) Create functions that can be used as the resultFunc of many different selectors

Indexing isn't for everyone, I'll let others suggest other methods.

这篇关于如何处理Redux中的关系数据?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆