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:
- Getting all Authors, which have at least one Book in specific category.
- 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
- As you can see, computing / querying relational data in selectors gets too complicated.
- Loading child relations (Author->Books).
- Filtering by child entities (
getHealthAuthorsWithBooksSelector()
).
- 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>
from How to deal with relational data in Redux?
This happens in the frameworks foundation and isn't yet exceptionally mainstream with a large portion of the organizations. Data Analytics Courses
ReplyDeleteI am looking for and I love to post a comment that "The content of your post is awesome" Great work!
ReplyDeletedata science course
I have to search sites with relevant information on given topic and provide them to teacher our opinion and the article.
ReplyDeleteSimple Linear Regression
Correlation vs Covariance
very well explained .I would like to thank you for the efforts you had made for writing this awesome article. This article inspired me to read more. keep it up.
ReplyDeleteSimple Linear Regression
Correlation vs covariance
data science interview questions
KNN Algorithm
Logistic Regression explained
This is a wonderful article, Given so much info in it, These type of articles keeps the users interest in the website, and keep on sharing more ... good luck.
ReplyDeletedata science interview questions
very well explained .I would like to thank you for the efforts you had made for writing this awesome article. This article inspired me to read more. keep it up.
ReplyDeleteSimple Linear Regression
Correlation vs covariance
data science interview questions
KNN Algorithm
Logistic Regression explained
I am looking for and I love to post a comment that "The content of your post is awesome" Great work!
ReplyDeleteSimple Linear Regression
Correlation vs Covariance
Really nice and interesting post. I was looking for this kind of information and enjoyed reading this one. Keep posting. Thanks for sharing.Data Analytics Course
ReplyDelete