diceline-chartmagnifiermouse-upquestion-marktwitter-whiteTwitter_Logo_Blue

Today I Learned

TypeScripts Writable Computed Refs

Writable Computed Refs can be of real help when using Vue's Composition API along with TypeScript. Let's see what they are and how we can use them by getting through an example.

Problem description: let's assume we have to display a dialog with all our online users, if there are any. At the same time, we have the option to disable dialogs on our website, for some reason. In order achieve our goals, we are using an UI library like PrimeVue, Ant or Vuetify. These assist us with a reusable Dialog component that is visible when a boolean allows it to do so, something like this:

<Dialog 
   v-model:visible="isDialogVisible"
>

The problem that we encounter is that of declaring our isDialogVisible reactive variable. What are the problems you may think.

  1. It should both take into consideration if we have something to display in our list (if we have users in our store - using a getter for that) AND if we are allowed to display dialogs on our website. So it cannot be a simple reactive reference.

  2. You may think: alright, it's not really a problem, that's what computed properties are for. They are reactive and build up a value based on one or more raw variables. So this should solve the problem:

const isModalVisible = computed(() => useStore().getters.getOnlineUsers.length > 0 && areModalsAllowed.value);

Theoretically that's right, but it does not really solve our specific problem, because our dialog must also be closed in 3 scenarios (that means our isDialogVisible variable should be set to false): a. when dialogs are disabled on the website b. when we click on of the dialog's close button c. when the getter returns an empty list of users

The problem is that computed properties are not overridable. Depending on how you do it you could get one of the following warnings that cause unexpected behaviour and make the application not respond to the users commands of closing the dialog:

[Vue warn]: Computed property was assigned to but it has no setter.
[Vue warn]: Write operation failed: computed value is readonly.

Of course, it's a bad idea to empty the users list in the store and make the dialogs disabled on the website when you click on the close button, or to disable the dialogs on the website when there are no users in the store, or... you got the idea.

Here's where our Writable Computed Refs come into play and help us gracefully solve our problem:

const isDialogVisible: WritableComputedRef<boolean> = computed({
      get: (): boolean => useStore().getters.getOnlineUsers.length > 0 && areModalsAllowed.value,
      set: (newValue: boolean): void => { areModalsAllowed.value = newValue }
    });

Explanation: We can specifically state the generic WritableComputedRef<T> type and construct our isDialogVisible boolean by providing an object with specific get and set methods to computed().
Our set method will be automatically used by the Dialog component when having to close the dialog due to the "Disable dialogs" button's click event. But when one checks for its value, our provided get method will also take the getters value into consideration.

We can understand this as surrounding(proxying) our computed property with custom get and set methods while it stays reactive.

How to dynamically render components in "vue-infinite-loading" using relative heights

When working with an infinite loading library like "vue-infinite-loading" or "vue-infinite-scroll", instead of hardcoding the amount of components to be rendered per page, we might use a technique to estimate the right number, based on the height of the browser window and the height of the component.

We need to obtain both of those heights and then divide them to obtain the number we are looking for. First, we can obtain the height of the browser window by using the following command:

window.innerHeight

After that, in order to obtain the height of the components rendered in the infinite scroll window, we may use:

this.$refs.yourComponentRefHere.clientHeight

"clientHeight" will return the height of the component and now, we can divide the numbers obtained to get the render number. ( Note: as a safety measure, the number we get should be larger than the obtained number, therefore we need to add a number to it to be sure it will behave as desired).

By using this technique we can change the render number in a responsive manner and can avoid unnecessary requests in order to give the user a more performant user experience.

Note: We can also dynamically adjust the render number if the window size changes, by adding a watcher and the following event listener:

window.addEventListener('resize',onResize)

Where "onResize" is the action to re-calculate the render number.

Making only some state properties persistent inside a persistent Vuex module

Making data persistent in web applications is a common required feature. State management patterns and libraries like Vuex provide systems for storing data in the local storage for achieving the persistence of data, making the data survive over refresh and making it reusable without having to request the data again.

The local storage has a (configurable) memory limit. Overflowing can cause unwanted behaviour and make the app work unexpectedly. Vuex gives the opportunity to make only some of its modules persistent, which is great.

Step 1: We import the modules and group them together in our modules object:

import store as module1 from '../module1Location'
import store as module2 from '../module2Location'
import store as module3 from '../module3Location'

const modules = {
  module1,
  module2,
  module3,
};

Step 2: For choosing which modules should be persistent, we can use the vuex-persistedstate plugin. We have to define the plugins array that will be passed to the createStore method like this:

const plugins = [
  createPersistedState({
    storage: window.localStorage,
    key: 'yourkey',
    paths: [
      'module1',
      'module2',
      'module3',
    ],
  }),
];

Step 3: Having only some modules to be persistent is nice. But what if we need to only make some parts of a module persistent? What if one of our modules should have all its state properties persistent except for one, the one that causes the unwanted local storage overflow?

Lets pretend our module3 has a property named bigData that causes the overlow.

Solution: we can add the REDUCER FUNCTION to our payload for the createPersistedState() function. Here is how our plugins array should look like:

const plugins = [
  createPersistedState({
    reducer: (state: RootState) => {
      const { module3, ...restOfRootState } = state;
      const { bigData, ...restOfModule3State } = module3;
      return {
        ...restOfRootState,
        module3: { ...restOfModule3State },
      };
    },
    storage: window.localStorage,
    key: 'yourkey',
    paths: [
      'module1',
      'module2',
      'module3',
    ],
  }),
];

Explanation of the reducer function: The reducer function takes in a state and returns the new state that should be persisted. We can change the state that we receive initially to only contain the data that we wish. Lets see how we extract the problematic bigData out of our module.

  1. We get our RootState as a parameter. This is an object containing all our Vuex modules.
  2. We extract in a variable our problematic module3 out of this RootState object and keep all other (non-problematic) modules in another separate object using the rest operator.
  3. We extract our problematic bigData from our module3 variable (the one extracted at step2) and keep all other (non-problematic) state properties in another object using the rest operator.
  4. We return the new state object that we build by spreading out the object containing the non-problematic modules using the spread operator and by redefining our module3 that should now contain only the non-problematic state properties (all its previous properties except bigData).

Step 4: passing the modules and plugins objects to the createStore() function, through its payload.

export const store = createStore({
  modules,
  plugins,
});

After this, the local storage will only help persist our modules, without the module3's bigData object that was causing our local storage overflow.

How to add dynamic routes with VUE router

Dynamic routes are an important part. Let's consider we have a blog (for example) which has 10 posts, each of them with a unique URL. Creating a new component for each blog post isn't the best practice, so here intervenes a special feature of VUE: dynamic routes.

First, let's create a Post component which will serve as a template for displaying each unique blog post:

<template>
	<div>
		<h1>Welcome to Post page</h1>
		<p>This is the page for post number {{ $route.params.id }}</p>
	</div>
</template>

Now, we need to update the router/index.js with our new route. *Don't forget to include the import for the Post component.

Below, we will use :id parameter as a dynamic value in the route:

{
	path: '/post/:id',
	name: 'Post',
	component: Post
}

Nicely done, you have now dynamic routes for your blog.

Vue + TypeScript App architecture with autonomous service layer implementation

Goal: having an autonomous, central layer for the different services defined in their own modules that will provide all registered services through a custom hook

Step 1: in a new TypeScript file, import all needed services from their modules

import { Service1 } from '@/location1/Service1'
import { Service2 } from '@/location2/Service2'
import { Service3 } from '@/location3/Service3'

Step 2: define the object containing the services, so that we will be able to retrieve the services from it. New services will be imported and registered in this object. After that, extract its type for further need.

const services = {
   service1: Service1,
   service2: Service2
   service3: Service3,
};

type AvailableServices = typeof services

Step 3: define a ServiceProvider class that will internally hold the services object

class ServiceProvider {
  constructor(protected services: AvailableServices) {}

  public provide( serviceName: keyof AvailableServices)
  :InstanceType<AvailableServices[keyof AvailableServices]> {
    return new this.services[serviceName]();
  }
}

Note 1: constructor automatically initialising protected class member "services", no explicit initialiser needed

Note 2: the generic return type of the provide method. It creates the returned type instance based on the constructor function of the service returned

Step 4: create a service provider instance

const serviceProvider = new ServiceProvider(services);

Step 5: define the magic hook that we will be calling from all components for making use of the above defined logic

export function useService( serviceName: keyof AvailableServices,)
:InstanceType<AvailableServices[keyof AvailableServices]> {
  return serviceProvider.provide(serviceName);
}

How to use it inside components:

import { useService } from '@/itsLocation';

const service1 = useService('service1');

Nice to have as a future implementation: each service as a singleton