Data & State Management
git checkout 5-data-access
Simple Shared State
Open the browser dev tools and switch to the network tab. Use the filter to filter down to “XHR” requests. This shows calls made through the fetch
method we use to load session data.
Keep an eye on that as you navigate back and forth between the sessions list and the session detail page.
Even though we’ve already loaded the list of sessions from /api/sessions/index.json
, each time we come back to the session list page it reloads the data. It sure would be nice if we could hang on to that data and skip reloading it! In our local environment, the API is very fast, but imagine if you were on the road accessing the page from your phone in spotty cellular coverage. Anything we can do to avoid unnecessary data loading would be really helpful.
Each time we come back to the <SessionList />
component, the code in the <script>
block runs anew. The onMounted
callback runs again, which triggers the data load.
However, if we pull the ref
outside of the component, we can check to see if it has data before reloading from the network.
Refactoring to use a composable
First, let's refactor a little bit. Create a new file called useSessions.ts
. Note this time, it is not a .vue
file: we’re going to write some plain old typescript.
We’re basically going to cut and paste some of the code from the <script>
block in SessionList.vue
and paste it into a new useSessions
function inside this new file:
import { onMounted, ref } from 'vue';
import { SessionOverview } from '../models.ts';
export function useSessions() {
// BEGIN PASTE FROM SessionList.vue
const sessions = ref<SessionOverview[]>([]);
onMounted(() => {
fetch('/api/sessions/index.json')
.then((res) => res.json())
.then((data) => {
sessions.value = data;
console.log(data);
});
});
// END PASTE FROM SessionList.vue
return sessions;
}
We’ve created a function and cut and pasted a few lines out of SessionList.vue
. Update the vue component to use this function instead:
import SessionCard from './SessionCard.vue';
import ButtonGroup from './ButtonGroup.vue';
+ import { useSessions } from './useSessions.ts';
- // we 'll talk about this in a later module, but here we load the list of sessions and log it to the browser console
- // you can also open public/api/sessions/index.json to see the data in your editor
- const sessions = ref<SessionOverview[]>([]);
- onMounted(() => {
- fetch('/api/sessions/index.json')
- .then((res) => res.json())
- .then((data) => {
- sessions.value = data;
- console.log(data);
- });
- });
+ const sessions = useSessions();
const searchText = ref('');
This is bog-standard javascript refactoring: we create a new function in a new file, and then moved a few lines to that function.
Congratulations, you just made your first vue compsable. Composables are a great mechanism for sharing code between vue components. Now any component that wants to load and operate on the list of sessions can just call useSessions()
.
All of the standard Vue functions like ref
, computed
, onMounted
, etc can be extracted out to higher level functions you can use to share that logic.
If you try it out in your browser you’ll see that it still doesn't cache the list of sessions: it loads it from the server each time you visit the page. But that makes sense, since all we did was move some typescript code from one file to another — it should behave the same.
Let’s make it remember the session data and avoid reloading.
Dead Simple State Management
Move the sessions
declaration outside the function.
Then add an if
check inside of onMounted
. If the sessions list has data, return early and skip hitting the api.
import { onMounted, ref } from 'vue';
import { SessionOverview } from '../models.ts';
+ const sessions = ref<SessionOverview[]>([]);
+
export function useSessions() {
- const sessions = ref<SessionOverview[]>([]);
onMounted(() => {
+ if (sessions.value.length > 0) {
+ return;
+ }
+
fetch('/api/sessions/index.json')
.then((res) => res.json())
.then((data) => {
That’s it! Now we have dead-simple caching of our session list. Since the sessions Ref is defined outside the function, there is only ever one instance no matter how many times useSesions()
is called. Each call uses the same data.
If you watch the network tools now, you’ll see that it only requests api/sessions/index.json
once, no matter how many times you navigate back and forth. The data is stored in-memory, and will persist until the page is refreshed through the browser’s reload button, or the tab is closed.
There are many more powerful state management libraries you can install and try. They all work on similar concepts: using Vue refs
behind the scenes and giving you access to composables to leverage inside your component.
Adding a more advanced state library
The naive composable we made in the previous section is good for many simple data loading scenarios, but it got some issues. For example:
- theres no way to invalidate the cache and load again — data could get stale
- theres technically a race condition where two components using
useSessions()
could fire off requests for the data at the same time
- theres no error handling or loading state support - what if we wanted to show a spinner while the data was loading?
To address some of that, we’re going to play with vue-query
, a port of the very popular react-query
library.
Bring the library in by running npm install --save @tanstack/vue-query
.
Now we have to add its plugin to our application. Update main.ts
to import VueQueryPlugin
from @tanstack/vue-query
and put it into the pipeline:
import SessionList from './components/SessionList.vue';
+ import { VueQueryPlugin } from '@tanstack/vue-query';
...
- createApp(App).use(router).mount('#app');
+ createApp(App).use(router).use(VueQueryPlugin).mount('#app');
Now we can replace our custom useSessions
composable with one powered by vue-query
:
The whole file will look like this:
import { SessionOverview } from '../models.ts';
import { useQuery } from '@tanstack/vue-query';
function fetchSessions(): Promise<SessionOverview[]> {
return fetch('/api/sessions/index.json').then((res) => res.json());
}
export function useSessions() {
return useQuery({
queryKey: ['sessions'],
queryFn: fetchSessions,
placeholderData: [],
staleTime: 1000 * 60, // 1 minute
});
}
We extracted a helper function called fetchSessions
that makes the actual network call. Then we replaced the implementation of useSessions()
with a call to useQuery()
It’s configured as follows:
queryKey
sets a list of unique names that can be used to identify the results of this query. If we want to manually invalidate the cache later, we can do so by name.
queryFn
stands for “query function” and it points to the method we use to actually load the data. Vue Query will call this function whenever it determines data needs to be loaded
placeholderData
defines the data that should be displayed during the first load of the API. In this particular use case, I’d like the composable to return an empty list of sessions while the request is inflight
staleTime
sets a 1 minute cache: if navigate away from the list and back within 1 minute,vue-query
will reuse the data and skip making another network call
Back in the SessionList we have to make one small change to hook in:
import ButtonGroup from './ButtonGroup.vue';
import { useSessions } from './useSessions.ts';
- const sessions = useSessions();
+ const { isLoading, isError, data: sessions } = useSessions();
const searchText = ref('');
The useSessions
composable now returns an object with multiple properties:
isLoading
is a boolean indicating if the network request is currently in flight. You could use this to show a loading spinner
isError
tells you if there was a problem. You can use it to show an error message
data
, which we’ve renamed into a variable calledsessions
for backwards compatibility. It holds the results of the API call
vue-query
can do a lot of other things too. You can have fine-grained control over caching, invalidation, mutations, etc. Fully covering its capabilities would be a workshop in itself. Check out the docs and try out some of its new more powerful features.
Recap
- You learned how to extract logic from a component into shareable composables
- You learned how to reuse a Ref across many lifetimes of a component to avoid reloading data
- You learned how to install and configure vue-query for managing server state
- You learned how to create a custom composable backed by data managed through vue-query
Further Reading
Bonus
- show a loading spinner while the sessions list is loading. Tip: in the browser developer tools, you can emulate slow networks on Network tab. What’s the difference between
isLoading
andisFetching
?
- Update
SessionDetails.vue
to usevue-query
. Review the docs on how to to use query keys for individual items, rather than a list.