VueX: How to architecture my store with nested objects
Asked Answered
L

1

6

I am currently coding an application in VueJS (and with Vuex in particular). However, my question is not strongly linked to this library, but rather to the architecture to have with a store like flux/redux/Vuex.

To put it simply, I have several APIs (one API/database per team), and for each team/API, I have several users.These teams and users are represented by simple objects, and each has its own slug. Important note: the slugs of the teams are of course unique, but the slugs users are unique for their own team. The uniqueness constraint for a user would then be "teamSlug/userSlug". And given the large number of users, I can not simply load all the users of all the teams.

My question is how to properly architect my application/store in order to recover the data of a given user slug (with his team): if I have not already loaded this user, make an API request to retrieve it. Currently I have created a getter that returns the user object, which takes the slug from the user and the team. If it returns "null" or with a ".loading" to "false", I have to run the "loadOne" action that will take care of retrieving it:

import * as types from '../../mutation-types'
import users from '../../../api/users'

// initial state
const state = {
  users: {}
}

// getters
const getters = {
  getOne: state => (team, slug) => (state.users[team] || {})[slug] || null
}

// actions
const actions = {
  loadOne ({ commit, state }, { team, slug }) {
    commit(types.TEAM_USER_REQUEST, { team, slug })
    users.getOne(team, slug)
      .then(data => commit(types.TEAM_USER_SUCCESS, { team, slug, data }))
      .catch(error => commit(types.TEAM_USER_FAILURE, { team, slug, error }))
  }
}

// mutations
const mutations = {
  [types.TEAM_USER_REQUEST] (state, { team, slug }) {
    state.users = {
      ...state.users,
      [team]: {
        ...(state.users[team] || {}),
        [slug]: {
          loading: true,
          error: null,
          slug
        }
      }
    }
  },

  [types.TEAM_USER_SUCCESS] (state, { team, slug, data }) {
    state.users = {
      ...state.users,
      [team]: {
        ...(state.users[team] || {}),
        [slug]: {
          ...data,
          slug,
          loading: false
        }
      }
    }
  },

  [types.TEAM_USER_FAILURE] (state, { team, slug, error }) {
    state.users = {
      ...state.users,
      [team]: {
        ...(state.users[team] || {}),
        [slug]: {
          slug,
          loading: false,
          error
        }
      }
    }
  }
}

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations
}

You imagine that a team does not only have users, I have many other models of that type, and I should link them together. This method works, but I find it rather cumbersome to put in place (especially that it is a simple get, I will have plenty of other actions of this kind). Would you have any advice on my architecture?

Thank you!

Lane answered 20/4, 2017 at 15:40 Comment(0)
C
1

I've found that the best way to keep the Vuex store flexible is to normalize it and keep your data items as flat as possible. That means storing all of your users in one structure and finding a way to uniquely identify them.

What if we combine the team and user slug to create a unique identifier? Here's how I imagine your users with a red team and a blue team:

const state = {
  users: {
    allTeamSlugs: [
      'blue1',
      'blue2',
      'blue3',
      'red1',
      'red2',
      // etc...
    ],
    byTeamSlug: {
      blue1: {
        slug: 1,
        team: 'blue',
        teamSlug: 'blue1'
      },
      // blue2, blue3, etc...
      red1: {
        slug: 1,
        team: 'red',
        teamSlug: 'red1'
      },
      // red2, etc...
    }
  }
}

And the teamSlug property doesn't need to exist for each user in your API. You can create it in your mutation as you load data into the store.

const mutations = {
  [types.TEAM_USER_SUCCESS] (state, { team, slug, data }) {
    const teamSlug = [team, slug].join('')
    state.users.byTeamSlug = {
      ...state.users.byTeamSlug,
      [teamSlug]: {
        ...data,
        slug: slug,
        team: team,
        teamSlug: teamSlug
      }
    }
    state.users.allTeamSlugs = [
      ...new Set([ // The Set ensures uniqueness
        ...state.users.allTeamSlugs,
        teamSlug
      ])
    ]
  },

  // ...
}

Then your getters might work like this:

const getters = {
  allUsers: state => {
    return state.users.allTeamSlugs.map((teamSlug) => {
      return state.users.byTeamSlug[teamSlug];
    });
  },
  usersByTeam: (state, getters) => (inputTeam) => {
    return getters.allUsers.filter((user) => user.team === inputTeam);
  },
  getOne: state => (team, slug) => { // or maybe "userByTeamSlug"?
    const teamSlug = [team, slug].join('');
    return state.users.byTeamSlug[teamSlug]; // or undefined if none found
  }
}

Redux has a great article about normalization that I always find myself coming back to: https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape#designing-a-normalized-state

Cucumber answered 22/4, 2019 at 14:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.