javascript – 如何在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商店
商店采用扁平结构,符合Redux最佳实践 – Normalizing State Shape.
这是书籍和作者商店的初始状态:
const initialState = {
// Keep entities, by id:
// { 1: { name: '' } }
byIds: {},
// Keep entities ids
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);
<查看/>组件仅用于演示.它将虚拟数据推送到Store并将所有Presentation组件呈现为< 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]) ));
当你有一个选择器时,它会变得混乱,它会计算/查询关系数据.
演示应用程序包括以下示例:
>获取所有作者,其中至少有一本书属于特定类别.
>获得相同的作者,但与他们的书籍一起.
以下是令人讨厌的选择器:
/**
* 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])
}))
));
加起来
>正如您所看到的,在选择器中计算/查询关系数据变得过于复杂.
>加载子关系(作者 – >书籍).
>按子实体过滤(getHealthAuthorsWithBooksSelector()).
>如果实体有很多子关系,那么选择器参数会太多.检查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>
解决方法:
这让我想起了我是如何开始我的一个数据高度关系的项目.你对后端的做事方式还有太多的考虑,但是你必须开始考虑更多的JS做事方式(对某些人来说这是一个可怕的想法,当然).
1)状态中的标准化数据
您已经很好地规范了数据,但实际上,它只是在某种程度上正常化了.我为什么这么说?
...
books: [1]
...
...
authorId: 1
...
您在两个地方存储了相同的概念数据.这很容易变得不同步.例如,假设您从服务器收到新书.如果它们都具有1的authorId,您还必须修改本书并将其添加到它中!这是很多额外的工作,不需要做.如果没有完成,数据将不同步.
具有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: '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),
});
啊,这个组件真正需要的唯一数据!书籍和作者.使用其中的数据,它可以计算它需要的任何东西.
请注意,我将它从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来反规范化的东西,对吧?
正确!但在这种情况下,我们不会占用带有冗余信息的内存.事实上,每个单独的实体,书籍和作者,都只是对商店中原始对象的引用!这意味着唯一占用的新内存是容器数组/对象本身,而不是它们中的实际项.
我发现这种解决方案适用于许多用例.当然,我没有将它保存在上面的组件中,我将其提取为可重用的函数,该函数根据特定条件创建选择器.
虽然,我承认我没有遇到与您相同复杂性的问题,因为您必须通过另一个实体过滤特定实体.哎呀!但仍然可行.
让我们将索引器函数提取到一个可重用的函数中:
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的嵌套版本的数据,使用固有的链接能力使用您已有的工具获取数据:ID和对象引用.
这些原则甚至可以应用于您当前的选择器.而不是为每个可想到的数据组合创建一堆高度专业化的选择器……
1)根据特定参数创建为您创建选择器的函数
2)创建可用作许多不同选择器的resultFunc的函数
索引不适合所有人,我会让其他人建议其他方法.
标签:javascript,reactjs,redux,react-redux,reselect 来源: https://codeday.me/bug/20190930/1834739.html