How to store relational data inside Redux

pigozzifr

Francesco Pigozzi

Posted on April 3, 2019

How to store relational data inside Redux

Photo by Tobias Fischer on Unsplash

Sooner or later, all frontend developers need to save relational data in a Redux store.

TL;DR

Follow the advice of Firebase, they are very useful.

Data

Whether they come from APIs or static payloads, based on relational DBs or not, the data has relationships between them.

We will define resources all those types of data that could have relations with other resources.

An example resource could be a post from our personal blog.

This post will have a 1:1 relationship with the author (from the point of view of the post, we will see that an author will have a 1:N relationship with posts) and a 1:N relationship with comments.

An inefficient structure

Let's say that our personal blog should only present a list of posts and a detail page for each of them.

For this specific example we could have, in our Redux store, a first level key containing all our posts indexed by id. Within each one we could nest the author's data and comments.

const store = {
  posts: {
    'p123': {
      id: 'p123',
      title: 'Best React pattern in the world!',
      author: {
        id: 'a123',
        name: 'Francesco',
        email: 'name.surname@gmail.com',
      },
      comments: {
        'c123': {
          id: 'c123',
          body: 'Best article ever!',
        },
        'c124': {
          id: 'c124',
          body: 'Best article ever ever!',
        },
      }
    },
  }
}

// And so on, you get the point
Enter fullscreen mode Exit fullscreen mode

The problem occurs if I wanted to create a page for the author's profile with a list of his posts.

To take an example with Redux selectors, we could retrieve the data we need in this way:

// This returns an array of posts
const getPostsByAuthor = authorId => state => (
  Object.values(state.posts).filter(post => post.author.id === authorId)
)

// And you'd call this selector like this:
const state = store.getState()
const postsByAuthor = getPostsByAuthor('a123')(state) // [...]
Enter fullscreen mode Exit fullscreen mode

But it would be particularly inefficient to be able to get what we need: every time we should go through all the posts.

A weighted structure

A weighted structure could be a 1:1 representation of the hypothetical tables within our relational DBs.

const store = {
  posts: {
    'p123': {
      id: 'p123',
      title: 'Best React pattern in the world!',
      author: 'a123',
    },
  },
  author_posts: {
    'a123': ['p123'],
  },
  authors: {
    'a123': {
      id: 'a123',
      name: 'Francesco',
      email: 'name.surname@gmail.com',
    }
  },
  post_comments: {
    'p123': ['c123', 'c124'],
  },
  comments: {
    'c123': {
      id: 'c123',
      body: 'Best article ever!',
      post: 'p123',
    },
    'c124': {
      id: 'c124',
      body: 'Best article ever ever!',
      post: 'p123',
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

In this case we removed the nesting problem. However, we added two new first level keys to our Redux store.

This approach is not totally wrong but, as our application grows, it may be difficult to manage all relationships efficiently.

It could be a usable approach if the amount of resources is limited. But it is also true that, if the amount of resources is limited, we may not really need Redux.

An efficient structure

Following the recommendations of Firebase, we could save ourselves some first level keys:

const store = {
  posts: {
    data: {
      'p123': {
        id: 'p123',
        title: 'Best React pattern in the world!',
        author: 'a123',
      },
    },
    comments: {
      'p123': ['c123', 'c124'],
    },
  },
  authors: {
    data: {
      'a123': {
        id: 'a123',
        name: 'Francesco',
        email: 'name.surname@gmail.com',
      },
    },
    posts: {
      'a123': ['p123'],
    },
  },
  comments: {
    data: {
      'c123': {
        id: 'c123',
        body: 'Best article ever!',
        post: 'p123',
      },
      'c124': {
        id: 'c124',
        body: 'Best article ever ever!',
        post: 'p123',
      },
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

Unlike Firebase, we are not going to nest relationships with "placeholders".

Instead, we organize our first-level keys as small second-level store containers.

Do you think of something similar thinking about the reducers and the combineReducers function? Same logic: we reduce our global object to the smallest representable portion.

Bonus: how to structure selectors

After having structured our Redux store, the first question that could come to your mind could be: how do I get this data?

Here are some simple selectors.

// Base data

const selectAuthors = state => Object.values(state.authors.data)
const selectAuthor = id => state => state.authors.data[id]

const selectPosts = state => Object.values(state.posts.data)
const selectPost = id => state => state.posts.data[id]

// Totally useless
const selectComments = state => Object.values(state.comments.data)
// Maybe useless
const selectComment = id => state => state.comments.data[id]

// Relations

const selectAuthorPosts = authorId => state => {
  const authorPosts = state.authors.posts[authorId] || []
  return authorPosts.map(postId => selectPost(postId)(state))
}

const selectPostComments = postId => state => {
  const postComments = state.posts.comments[postId] || []
  return postComments.map(commentId => selectComment(commentId)(state))
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Now you could structure a Redux store to save relational data. It could be overkill in some cases but it will come in handy to handle more complex applications.

Leave a 🦄 if you liked the post!

💖 💪 🙅 🚩
pigozzifr
Francesco Pigozzi

Posted on April 3, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related