Untangle application state

Natalia Tepluhina

Staff Frontend Engineer

Core Team Member

Google Dev Expert

@N_Tepluhina

State management is complicated

- Vuex

- something else (Redux/MobX/Xstate...)

- composables to share state

- Pinia

Pinia

import { defineStore } from 'pinia'

export const useStore = defineStore('main', {
  state: () => {
    return {
      counter: 0,
    }
  },
})
export const useStore = defineStore('main', {
  state: () => {
    return {
      counter: 0,
    }
  },
  actions: {
    increment() {
      this.counter++
    },
  },
})

- synchronous

- does not create a lot of boilerplate

- has one source of changes

- non-persistent

...and now server data comes into the game...

Looks simple, right?

export const useStore = defineStore('tractors', {
  state: () => {
    return {
      tractors: [],
      loading: false,
      error: null,
    }
  },
})
export const useStore = defineStore('tractors', {
  state: () => {
    return {
      tractors: [],
      loading: false,
      error: null,
    }
  },
  actions: {
    async fetchTractors() {
      this.loading = true
      try {
        const response = await getTractors()
        this.tractors = response.data
      } catch (error) {
        this.error = error
      }
      this.loading = false
    },
  },
})

Now let's add it to both components!

import { useStore } from '../store'

const store = useStore()
store.fetchTractors()

Woops

Ok fine let's deduplicate

export const useStore = defineStore('tractors', {
  state: () => {
    /* ... */
  },
  actions: {
    /* ... */
  },
  getters: {
    fetchStarted: (state) => state.tractors.length > 0 || state.loading,
  }
})
if (!store.fetchStarted) {
  store.fetchTractors()
}
[
  {
    id: '1',
    name: 'John Deere 7R 350 AutoPowr',
    image_url: 'https://tractors.com/1',
  },
  {
    id: '2',
    name: 'Reform Metrac H75 Pro',
    image_url: 'https://tractors.com/2',
  }
]
[
  {
    id: '1',
    name: 'John Deere 7R 350 AutoPowr',
    image_url: 'https://tractors.com/1',
    engine: '9-litre, 6-cylinder DPS',
    rated_power: '350hp',
    description:
      'The low overall machine weight of the 7R Series and high horsepower ...',
  },
  {
    id: '2',
    name: 'Reform Metrac H75 Pro',
    image_url: 'https://tractors.com/2',
  }
]

Sounds easy?

actions: {
  async fetchTractor(id) {
    this.loading = true
    try {
      const response = await getTractor(id)
      const existingTractor = this.tractors.find((t) => t.id === id)
      if (existingTractor) {
        const { description, engine, rated_power } = response.data
        existingTractor.description = description
        existingTractor.engine = engine
        existingTractor.rated_power = rated_power
      } else {
        this.tractors.push(response.data)
      }
    } catch (error) {
      this.error = error
    }
    this.loading = false
  },
},
actions: {
  async fetchTractors() {
    this.loading = true
    try {
      const response = await getTractors()
      this.tractors = response.data
    } catch (error) {
      this.error = error
    }
    this.loading = false
  },
},
actions: {
  async fetchTractors() {
    this.loading = true
    try {
      const response = await getTractors()
      this.tractors = response.data.reduce((acc, current) => {
        const existingTractor = acc.find((t) => t.id === current.id)
        if (!existingTractor) {
          return [...acc, current]
        }
        return acc
      }, this.tractors)
    } catch (error) {
      this.error = error
    }
    this.loading = false
  },
},

Now we're ok?

Haha no

getters: {
  fetchStarted: (state) => state.tractors.length > 0 || state.loading,
}
getters: {
  fetchStarted: (state) => state.tractors.length > 1 || state.loading,
}

- how to fetch only when data is not fetched already?

- how to keep my data up-to-date?

- what if two components start fetching simultaneously?

Not all states are born equal!

Global state

Global state

Server cache

Local state

Apollo Client

Thank you!

NataliaTepluhina

@N_Tepluhina