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.

How to disable WordPress parent menu link

Working with WordPress menus, I wanted a top-level navigation element to act as a trigger for a drop-down menu.

It took me some time to figure out how to make this navigation element not redirect anywhere, but it was so easy.

You need to take the following steps:

  1. Insert a custom link with any link address and label that you want.

  2. Click on the 'Edit Menu Item' and delete the link that you insert previously.

  3. Create the sub-menu with the wanted links.

  4. Save.

That's all, nothing more, nothing less.

How to create pagination for custom taxonomy in WordPress

To create a pagination for the custom taxonomy archive, you must enter the following snippet in the code:

function custom_tax_query_change( $query ) {
     if ( ! is_admin() && $query->is_tax( 'example_taxonomy_name' ) ) {
          $query->set( 'posts_per_page', 10 );
     }
}
add_action( 'pre_get_posts', 'custom_tax_query_change' );

The 'post_per_page' tell us how many posts we want to display per page. If you want more or less, you can change the value as you need.

How to dynamically display different types of cards based on a pattern

Let's say you have an array of cards inside which you want add more items.

Example: [card, card, card, itemOne, card, card, itemTwo, ...]

For this we will create another empty array that we will work with:

let posts = [];

const itemsTemplate = [itemOne, itemTwo];
let items = [itemOne, itemTwo];

for (let i = 0; i < cards.length; i++) {
  posts.push(cards[i]);

  if (i !== 0) {

    //next index is multiple of 3
    if ((i + 1) % 5 === 3) {
      if (items.length === 0) {
        items = itemsTemplate;
      }
      posts.push(items.pop());
    }

    //next index is multiple of 5
    if ((i + 1) % 5 === 0) {
      if (items.length === 0) {
        items = itemsTemplate;
      }
      posts.push(items.pop());
    }
  }
}