diceline-chartmagnifierquestion-marktwitter-whiteTwitter_Logo_Blue

Today I Learned

How to obtain reactivity in custom hooks while interacting with the local storage

This is how we can obtain reactivity in our custom React.js hooks while working with the local storage, using the Pub/Sub (Observer) design pattern (with TypeScript support).

The goal is to implement a "useLocalStorage" custom hook, which will abstract away the complexity of reading from and writing to the local storage. As we know, each custom hook instantiates its own state. That is a problem in our case because when one instance of the hook updates the local storage, the state copies held by all the other hook instances will be out of sync and will never be updated.

We can solve this issue using the following idea: we can mimic a centralized shared state between our custom hook instances by delegating the responsibility of holding these in sync with the local storage to a custom "manager", the Observer object.

Our custom hook will work based on these ideas:

  1. Custom Hook instances will subscribe their inner state updater functions to this manager
  2. Custom Hook instances will publish the new state to the manager when updating a key of the local storage
  3. The manager will trigger all subscriber functions and thus update the inner states of the custom hook instances.

The observer object:

export type Listener<EventType> = (event: EventType) => void;

export type ObserverReturnType<KeyType, EventType> = {
  subscribe: (entryKey: KeyType, listener: Listener<EventType>) => () => void;
  publish: (entryKey: KeyType, event: EventType) => void;
};

export default function createObserver<
  KeyType extends string | number | symbol,
  EventType,
>(): ObserverReturnType<KeyType, EventType> {
  const listeners: Record<KeyType, Listener<EventType>[]> = {} as Record<
    KeyType,
    Listener<EventType>[]
  >;

  return {
    subscribe: (entryKey: KeyType, listener: Listener<EventType>) => {
      if (!listeners[entryKey]) listeners[entryKey] = [];
      listeners[entryKey].push(listener);
      return () => {
        listeners[entryKey].splice(listeners[entryKey].indexOf(listener), 1);
      };
    },
    publish: (entryKey: KeyType, event: EventType) => {
      if (!listeners[entryKey]) listeners[entryKey] = [];
      listeners[entryKey].forEach((listener: Listener<EventType>) =>
        listener(event),
      );
    },
  };
}

export const LocalStorageObserver = createObserver<
  LOCAL_STORAGE_KEYS,
  string
>();

export const { subscribe, publish } = LocalStorageObserver;

The useLocalStorage custom hook (window checks are optional, depending on which environment this JavaScript will run on):

export function useLocalStorage<T>(key: LOCAL_STORAGE_KEYS, initialValue: T) {
  const [storedValue, setStoredValue] = useState(() => {
    if (typeof window === 'undefined') {
      return initialValue;
    }
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  LocalStorageObserver.subscribe(key, setStoredValue);

  const setValue = (value: T) => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      LocalStorageObserver.publish(key, valueToStore);
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      }
    } catch (error) {
      console.error(error);
    }
  };
  return [storedValue, setValue];
}

React custom hook for form state management

Goal: defining a custom useForm hook for managing the state of our forms. This can have some initial state, mainly used for precompleting update forms, but can also provide empty form fields. 




Problem: when we want to operate on a precompleted form, altough we pass all the data for our fields, the form is not being precompleted and its fields are empty.

export default function useForm(initial = {}) {
  const [inputs, setInputs] = useState(initial);



  const handleChange = (e) => {
    let { value, name, type } = e.target;

    if (type === 'number') {
      value = parseInt(value);
    }
    if (type === 'file') {
      [value] = e.target.files;
    }

    setInputs({
      ...inputs,
      [name]: value,
    });
  };

  const resetForm = () => {
    setInputs(initial);
  };

  const clearForm = () => {
    const blankState = Object.fromEntries(
      Object.entries(inputs).map(([key, value]) => [key, ''])
    );

    setInputs(blankState);
  };

  return {
    inputs,
    clearForm,
    resetForm,
    handleChange,
  };
}


This is what happens:

  1. We pass an initial state object which is undefined (server-side rendered) until the GQL query loads.
  2. This initial object (which is undefined) populates the form fields making them empty.
  3. After the query loads, the initial state object is repassed to the useForm hook, but the DOM is not rerendered => a possible solution is to make use of the useEffect() hook for forcing rerendering.
  4. We cannot watch for changes directly on the initial object and reassign it using setInputs, because it triggers the useEffect callback once again and again and again when altering its value, causing an infinite loop.
  5. The solution is to watch for changes on a string joined by the values of the initial object. When that changes from undefined to the GraphQL query results, the useEffect callback is called and it initializes and rerenders the fields correspondingly.


An example implementation could be:

const initialValues = Object.values(initial).join('');
useEffect(() => {
  setInputs(initial);
}, [initialValues]);



Now the form precompletion works fine using our custom useForm() hook.

Vuex composition helpers utility package

One big advantage of the Composition API over the Options API is that it lets us group our variables and methods how we want. Most of the time we group them together by feature/functionality, as they use/call one another and it's convenient to have them grouped together this way.

For all apps where we use Vuex for state management, we probably take advantage of the mapState, mapGetters, mapActions and mapMutations binding helpers. Unfortunately, this does not work as expected with the Composition API. But there is also a way of how we can replicate this behavior and also maintain the structure of our components as described at the beginning as an advantage.

We have to install the vuex-composition-helpers utility package:

npm i vuex-composition-helpers@next

and use it as follows in our component:

<script setup lang="ts">

import { useState, useActions, useGetters, useMutations } from 'vuex-composition-helpers'

const { globalMessage, onlineUsers } = useState(['globalMessage', 'onlineUsers'])
const { getGlobalMessage } = useGetters(['getGlobalMessage'])
const { UPDATE_MESSAGE, ADD_USER } = useMutations(['UPDATE_MESSAGE', 'ADD_USER'])
const { updateMessage, addOnlineUser } = useActions(['updateMessage', 'addOnlineUser'])


</script>

DefineEmits and DefineProps compiler macros with default values in TypeScript

This is how we:


a. use the defineEmits() and defineProps() compiler macros with Composition API's Script Setup and TypeScript

b. pass default values to our component's props

<script setup>
const props = withDefaults(
  defineProps<{
    message: string, 
    isActive: boolean
  }>(), 
  {
    message: 'Default message', 
    isActive: false
  }
);

const emit = defineEmits<{
  (e: "customEventName", payload?: object) : void
}>();
</script>

Script setup syntactic sugar for the Composition API in SFCs

Problem: When using the Composition API, we need to return all (reactive) variables from the setup() method in order to use them in the template section. This can make components huge, harder to read and maintain and also cause unwanted bugs if we forget to return any variables used in the template. More than that, we can use variables only by accessing their "value" property.

This is the basic structure of a component that we want to improve:

<script>
//Long list of imports 

import Child1 from 'child1-location';
import Child2 from 'child2-location';
import { ref } from 'vue';

export default defineComponent({
  name: 'CustomComponent',
  components: { Child1, Child2 },
		
  setup() {
	const variable1 = ref(1);
	const variable2 = ref(2);
	//...
	const variable100 = ref(100);

	//altering variables
	variable1.value = 'new value for variable1';

	return {
		variable1,
		variable2,
		.
		.
		.
		variable100,
	}
  }
});
</script>

<template>
	<Child1 />
	<Child2 />
	{{ variable1 }}
	...
	{{ variable100 }}
</template>

Solution: The structure and performance of this component can be improved by using the Script Setup syntactic sugar. 
The Script Setup is a compile-time syntactic sugar that is recommended to be used in SFCs with the Composition API. Let's rewrite the component from above using the Script Setup:

<script setup> 
	
import Child1 from 'child1-location';
import Child2 from 'child2-location';
import { ref } from 'vue';
	
const variable1 = ref(1);
const variable2 = ref(2);
//...
const variable100 = ref(100);

variable1 = 'new value for variable1';
</script>

<template>
        <Child1 />
	<Child2 />
	{{ variable1 }}
	...
	{{ variable100 }}
</template>

Important differences and advantages: 



  1. We don't need to return any variables from our script in order to use them in the template, reducing the number of lines and increasing readability and operability
  2. We don't need to use .value to access and modify reactive variable values
  3. We can directly use imported Child Components in our template without registering them
  4. Better runtime performance due to the behind the hood implementation of the template (as a render function without an intermediate proxy)
  5. TypeScript support for defining props and emitted events (will be described in a future TIL)

JavaScript event processing and bubbling

JavaScript event listeners and handlers can have some unnatural behaviour if we are not aware of how things work behind the hood. Let's understand what these unexpected behaviours could be and why things actually happen as they happen.

Let's pretend that we have the following DOM structure with 3 divs: grandparent, parent and child.

<div class="grandparent"></div>
  <div class="parent">
    <div class="child"></div>
  </div>
</div>

All 3 divs have their custom event handlers, like follows:

grandparent.addEventListener('click', () =>  console.log('Grandparent'));
parent.addEventListener('click', () =>  console.log('Parent'));
child.addEventListener('click', () =>  console.log('Child'));

If we click on the inner div (the child div), we would expect to call the child div's event handler, but actually we will see in the console that all 3 event handlers have been called, in the following order:

Child
Parent
Grandparent

Let's understand why this happens. Mainly, there are 3 phases in processing JavaScript events:

  1. Capturing phase
  • the DOM tree is parsed downwards until the caller element is found. Throughout its way, if there are nodes that explicitly require event execution in the capturing phase, their event handlers will be executed right away.
  1. Targeting phase
  • the caller element that triggered the event is being targeted
  1. Bubbling phase
  • the DOM tree is now parsed upwards until finding the document element, starting from the targeted/caller element. If on the way up we find elements with event handlers, they will be automatically called.

If we want to stop this behaviour of calling parent elements event handlers, we can use event.stopPropagation() on our last event handler that we want to be executed.

Axios Request and Response Interceptors

Axios interceptors come in handy when we need to track, register, or work with:

  1. Requests before leaving
  2. Responses before arriving
  3. Both 1 and 2

Thanks to interceptors, we can pass our own handlers/callbacks for the following cases:

  1. Before launching a request
  2. Catching an error at HTTP request launch
  3. Arrival of a response
  4. Catching an error at response arrival

This is an example of how we can use them:

// handlers for the request launch and for catching request launch error
axios.interceptors.request.use(
   (config) => {
      console.log('We are now preparing to launch the request!')
      return config;
   },
   (error) => Promise.reject(error),
);

// handlers for response interceptor and error response interceptor
axios.interceptors.response.use(
     (response) => {
        console.log('We received the response!');
        return response;
     },
     (error) => {
       if (error.response.status === 403) 
         return Promise.reject(error);
     }
},

Each Axios Instance can have its custom configuration and request and response interceptors. This can be very useful when the architecture of our application enforces/allows each of our services to use their own Axios Instance. This way, each service can have an Axios Instance with custom configuration and custom request/response interceptors.

This is how an Axios Instance factory utility function could look like in TypeScript:

export const createAxiosWithInterceptors = (
  requestConfig: AxiosRequestConfig = {},
  requestInterceptorHandlers: Partial<RequestInterceptorHandlers> = DefaultRequestInterceptor,
  responseInterceptorHandlers: Partial<ResponseInterceptorHandlers> = DefaultResponseInterceptor,
) => {
  const axiosInstance = axios.create(requestConfig);

  axiosInstance.interceptors.request.use(
    requestInterceptorHandlers.requestConfigHandler,
    requestInterceptorHandlers.requestErrorHandler,
  );

  axiosInstance.interceptors.response.use(
    responseInterceptorHandlers.responseHandler,
    responseInterceptorHandlers.responseErrorHandler,
  );

  return axiosInstance;
};

where we can define our types and default interceptors as follows:

type RequestConfigHandler = (config: AxiosRequestConfig) => AxiosRequestConfig;
type RequestErrorHandler = (error: AxiosError) => Promise<AxiosError>;
type RequestInterceptorHandlers = {
  requestConfigHandler: RequestConfigHandler;
  requestErrorHandler: RequestErrorHandler;
};

type ResponseHandler = (response: AxiosResponse) => AxiosResponse;
type ResponseErrorHandler = (error: AxiosError) => Promise<AxiosError>;

type ResponseInterceptorHandlers = {
  responseHandler: ResponseHandler;
  responseErrorHandler: ResponseErrorHandler;
};

const DefaultResponseInterceptor = {
  responseHandler: (response: AxiosResponse) => response,
  responseErrorHandler: (error: AxiosError) => Promise.reject(error),
};

const DefaultRequestInterceptor = {
  requestConfigHandler: (config: AxiosRequestConfig) => config,
  requestErrorHandler: (error: AxiosError) => Promise.reject(error),
};

In each of our services we can then use our factory method as follows:

// axios instance with custom request config received through constructor, custom request interceptor and default response interceptor
protected http: AxiosInstance = createAxiosWithInterceptors(
    this.requestConfig,
    {
      requestConfigHandler: (config) => {
        console.log();
        return config;
      },
    },
);

JavaScript reduce function performance enhancement

Time complexity of implemented algorithms is an aspect which for sure should be taken into consideration when working on websites with big amounts of data. In this manner, let us test out the performance of the mother and father of all JavaScripts higher order functions - Array.prototype.reduce.

Problem description: Our API provides us with an array of objects that looks something like:

const objects = [
   { id: 1, name: "name 1"},
   { id: 2, name: "name 2"},
   { id: 3, name: "name 3"},
]

and we want to create a lookup table out of it, like this:

{
   1: "name 1",
   2: "name 2",
   3: "name 3",
}

using reduce:

const lookup = objects.reduce((lookup, object) => ({
     ...lookup,
     [object.id]: object.name
}), {})

It works, but the time complexity of this algorithm is (unexpectedly) O(n^3), because of the internal calls of Object.assign() that needs to copy bigger and bigger objects from iteration to iteration.

This is what JavaScript does internally:

objects.reduce((lookup, object) => Object.assign(
            Object.assign(lookup, {}), 
            { [object.id]: object.name }
        ), 
    {}
);

Time taken:

    100 records: ~0.08ms
  1.000 records: ~1.45ms
 10.000 records: ~  18ms
100.000 records: ~2600ms
1 mill  records: ~breaks

Performance boost:

const lookup = objects.reduce((lookup, object) => {
    lookup[object.id] = object.name
    return lookup
}, {})

Time taken:

    100 records: ~0.045ms
  1.000 records: ~ 0.15ms
 10.000 records: ~  1.5ms
100.000 records: ~  3.2ms
1 mill  records: ~   20ms

Great performance improvement by avoiding Object.assign calls and new object creation in each iteration.

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 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.

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.

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