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:

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 fetchSessionsthat makes the actual network call. Then we replaced the implementation of useSessions() with a call to useQuery()

It’s configured as follows:

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:

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

Further Reading

Bonus