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
Local state vs. server cache
By Natalia Tepluhina
Local state vs. server cache
- 819