diceline-chartmagnifiermouse-upquestion-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];
}

How to install npm packages from your local machine

If you need to manually test a package you are developing and don't want to make a release each time you modify something, you can install it directly from your local machine.

You can achieve this by using the following commands, depending on the package manager you use:

yarn add [path-to-your-package]
npm install [path-to-your-package]

This will have the following equivalent in package.json:

...
"dependencies": {
    "[package]": "file:[relative-path-to-package]",
    ...
},

How to run a node command from the UI Vision RPA Chrome extension

How to run a node command from the UI.Vision RPA chrome extension

The anatomy of a basic UI Vision task looks like this:

{
  "Command": "XType",
  "Target": "Hello World${KEY_ENTER}",
  "Value": "",
  "Description": ""
},

It can be a very simple task such as typing some text in the before-selected input control. But it can also perform advanced sequences. These sequences can involve conditional structures, and system calls. It can even execute javascript in a sandboxed context.

The two most important parameters are Value and Target. But sometimes its usage is not very intuitive.

Luckily, the documentation is clear and accessible. As a quick link next to the command dropdown.

Let's call a node.js process sometime in our automation flow.

To do this, make sure you have the UI Vision extra X Modules installed. If you're running this on mac, make sure to also grant the necessary permissions.

The inconsistent usage of Value and Target made me spend some time figuring out how to split the command. I've found that this works:

{
  "Command": "XRun",
  "Target": "/My Path/.n/bin/node",
  "Value": "/My Path/automations/my_automation.js",
  "Description": ""
},

The Target is the binary we want to run, and the Value is the parameter we want to pass to it. In our case, a javascript file.

Another mention-worthy caveat: In the Chrome extension, you can't pass the Terminal as Target process due to security concerns.

How to use DocumentFragment to render large lists faster

Using DocumentFragment does the trick because it isn't part of the active document tree structure.

This means it doesn't interact with the main document until mounted.

Hence, it doesn't impact the performance when changes occur to its contents.

// Setup
const outlet = document.getElementById('#myList');
const fragment = new DocumentFragment();
const results = fetchResults(); // returns Array<Result>

We then go through each result and prepare it properly:

results.map((result, index) => {
    const li = document.createElement('li');
    li.textContent = result;
    li.classList.add('result');
    // We append to the DocumentFragment
    fragment.appendChild(li);
 });

Finally, we append the end result to the list:

outlet.appendChild(fragment);

If we were to append each item to the list outlet within the map above, it would have triggered a repaint/reflow every time we performed the action.

Netlify CMS relation widget with a list

As I was using Netlify CMS, I wanted to use a relation widget that would dynamically add content depending on the other collection title field.

It's really easy to do this, but I struggled a little bit with it because the documentation wasn't so explicit, and I could not find something useful on the internet.

So here's how you do it:

This is the collection that will have dynamic content, depending on how many call-to-actions you'll create.

  - name: 'call-to-action'
    label: 'Call to action'
    path: "{{slug}}"
    create: true
    folder: 'content/call-to-action'
    fields:
      - {label: 'CTA Category', name: 'ctaCategory', widget: 'string'}
      - label: 'CTA Content'
        name: 'cta'
        widget: 'list'
        fields:
        - {label: 'Title', name: 'title', widget: 'string' }
        - {label: 'Content', name: 'content', widget: 'string' }
        - {label: 'cover', name: 'cover', widget: 'image', media_folder: '/assets/images/covers/'}
        - label: 'Button'
          name: 'button'
          widget: 'object'
          fields:
          - {label: 'url', name: 'url', widget: 'string'}
          - {label: 'title', name: 'title', widget: 'string'}

And now we have to link this collection to a field in the blog collection. The field in the blog collection looks like this :

- { label: 'Call to action category',
 name: 'ctaCategory',
 widget: 'relation', 
 collection: 'call-to-action', 
 search_fields: ["ctaCategory"], 
 value_field: "cta.*", 
 display_fields: ["ctaCategory"]}

The search will be done depending on what "ctaCategory" you select, and to get all the items in the list widget from the "call-to-action" collection, we need to specify in the value field that we want to get every item, and you do this with ".*".

That's all .

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>