Search code examples
vue.jsvuex

How to update one array that depends on another in Vuex


I have a search field and I want to filter users based on it. I show users if there is no filter or filteredUsers if you search anything.

The issue is that if you search in the search bar and after that I'm trying to edit a field (inline edit), users is updated, but filteredUsers is not.

I have also other filters and actions, that's why I've posted only that part of the code. I have to use both of them because in other cases I need users.

How should this work in vue.js?

//in componnet 
computed: {
      ...mapGetters([
        'users',
        'filteredUsers'
      ]),
      getFilteredUsers () {
        return this.filteredUsers || this.users
      }
    }

// in mutation
   UPDATE_CONSUMER (state, { user, index }) {
       // here users is updated, but filteredUsers in not
        state.users.splice(index, 1, user)
  },
<tr v-for="user, index in getFilteredUsers">

// getters.js file
export default {
  users: state => {
    return state.users
  },
  filteredUsers: state => {
    return state.filteredUsers
  }
}

// mutations.js
SEARCH_USERS (state) {
    const searchTextTrimmed = state.searchText.trimStart()

    const users = [
      ...state.users.filter(user => {
        return user.name.toLowerCase().startsWith(searchTextTrimmed.toLowerCase())
      })
    ]
    if (searchTextTrimmed) {
      state.filteredUser = users
    } else {
      state.users = users
      state.filteredUser = null
    }
  }


Solution

  • Ideally you wouldn't have both arrays in the state. Instead you'd just have users and searchText. You'd then have filteredUsers in the getters, derived from users and searchText.

    In the example below I've tried to preserve the UPDATE_CONSUMER mutation from the question. I extrapolated from there to assume that you want to create copies of the user objects when they are edited rather than mutating the original objects. The whole question becomes a bit moot if mutating the original objects is an option as the objects are shared between the two lists.

    const store = new Vuex.Store({
      state: {
        searchText: '',
        
        users: [
          {name: 'Black', fruit: 'Apple'},
          {name: 'Blue', fruit: 'Pear'},
          {name: 'Brown', fruit: 'Banana'}
        ]
      },
      
      mutations: {
        UPDATE_CONSUMER (state, { user, index }) {
          state.users.splice(index, 1, user)
        },
    
        UPDATE_SEARCH_TEXT (state, searchText) {
          state.searchText = searchText
        }
      },
      
      getters: {
        filteredUsers (state) {
          const searchText = state.searchText.trimStart().toLowerCase()
          const users = state.users
          
          if (!searchText) {
            return users
          }
    
          return users.filter(user => user.name.toLowerCase().startsWith(searchText))
        }
      }
    })
    
    new Vue({
      el: '#app',
      store,
      
      computed: {
        ...Vuex.mapState(['users']),
        ...Vuex.mapGetters(['filteredUsers']),
        searchText: {
          get () {
            return this.$store.state.searchText
          },
          set (searchText) {
            this.$store.commit('UPDATE_SEARCH_TEXT', searchText)
          }
        }
      },
      
      methods: {
        onInputFruit (user, fruit) {
          const newUser = {...user, fruit}
          const index = this.users.indexOf(user)
          
          this.$store.commit('UPDATE_CONSUMER', {user: newUser, index})
        }
      }
    })
    #app > * {
      margin: 0 0 10px;
    }
    
    table {
      border-collapse: collapse;
    }
    
    td, th {
      background: #eee;
      border: 1px solid #777;
      padding: 5px;
    }
    <script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/vuex.js"></script>
    
    <div id="app">
      <input v-model="searchText">
      <table>
        <tr><th>Name</th><th>Fruit</th><tr>
        <tr v-for="user in filteredUsers" :key="user.name">
          <td>{{ user.name }}</td>
          <td>
            <input :value="user.fruit" @input="onInputFruit(user, $event.target.value)">
          </td>
        </tr>
      </table>
      <p>{{ filteredUsers }}</p>
      <p>{{ users }}</p>
    </div>

    Some other points to note:

    1. I've got rid of the getter for users and used mapState instead. If you'd rather do everything via mapGetters you could put that back the way it was.
    2. You shouldn't need getFilteredUsers. Instead just have filteredUsers always return the current list of users matching the search criteria. I've included an optimisation to return early if there is no searchText but it would still work without it.
    3. In your code you have const users = [...state.users.filter. There's no need to use [...] to create a new array here as filter will be returning a new array anyway.
    4. I've kept the searchText and filtering in the store but that feels a bit like it should be handled by component state instead. It's difficult to say without knowing a lot more about your application but the search box is likely to be local to a small portion of the UI and that doesn't necessarily need to be shared with the rest of the application via the store.

    A potential problem with the way I've implemented this is that the data is updated immediately as the user types. It isn't a problem in my example but if the list is filtered based on the field being edited that could cause that row to suddenly disappear as the user is typing. One way to avoid that problem is to wait for the field to blur before updating the data, e.g. using the change event instead of the input event. Other workarounds are available depending on precisely what the expected behaviour should be.