<onWebFocus />

Knowledge is only real when shared.

JavaScript: The Magic Parts

September 10, 2023

Rarely used but extremely handy features built into the language.

Certain JavaScript and Web Development features often go unnoticed because they may appear daunting at first glance. However, these often-overlooked capabilities have the potential to significantly enhance your productivity. In this blog post, we'll delve into some of these features, shedding light on how they can empower you in your Web Development journey.

Proxy

A Proxy introduces an extra layer around regular JavaScript values, enabling the interception of any action on these values and facilitating the return of an appropriate response. In the interactive code example below, we've set up a Proxy with the most common methods configured to intercept actions, which are then displayed in the rendered history within the preview. Feel free to experiment by entering and executing commands like proxy.value = 5 (which uses the set method) and then proxy.value (which uses the get method) to access and view the previously assigned value.

import { useState } from 'react'
import { createRoot } from 'react-dom/client'
import { readableValue, addToHistory, setHistory, getHistory } from './helper'

// proxy object required to be in scope to run eval() code accessing it.
const proxy = new Proxy(
  {},
  {
    get(target, prop) {
      addToHistory('get', prop)
      return target[prop]
    },
    set(target, prop, value) {
      addToHistory('set', `${prop} to ${readableValue(value)}`)
      target[prop] = value
      return true
    },
    deleteProperty(target, prop) {
      if (prop in target) {
        addToHistory('delete', prop)
        delete target[prop]
        return true
      }
      return false
    },
    defineProperty(target, prop, descriptor) {
      addToHistory('define', prop)
      Reflect.defineProperty(target, prop, descriptor)
      return true
    },
    ownKeys(target) {
      addToHistory('keys', readableValue(Reflect.ownKeys(target)))
      return Reflect.ownKeys(target)
    },
  }
)

export default function App() {
  const [code, setCode] = useState('')
  const [result, setResult] = useState('')
  setHistory(useState([]))

  return (
    <div style={{ marginTop: 70 }}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 15 }}>{getHistory()}</div>
      <div
        style={{
          position: 'fixed',
          top: 0,
          left: 0,
          right: 0,
          display: 'flex',
          flexDirection: 'column',
          padding: 10,
          gap: 5,
          backgroundColor: '#F5F5F5',
        }}
      >
        <span>
          Example:{' '}
          <span style={{ fontFamily: 'monospace' }}>proxy.myProperty = 5 / Object.keys(proxy)</span>
        </span>
        <div style={{ display: 'flex', gap: 10 }}>
          <input
            placeholder="Enter Code"
            style={{ fontFamily: 'monospace', border: '1px solid #9E9E9E', borderRadius: 5 }}
            value={code}
            onChange={(event) => setCode(event.target.value)}
            onKeyDown={(event) => event.key === 'Enter' && setResult(eval(code))}
          />
          <button
            type="button"
            onClick={() => setResult(eval(code))}
            style={{ border: '1px solid #9E9E9E', borderRadius: 5 }}
          >
            Run
          </button>
          {result !== '' && <p style={{ margin: 0 }}>Return value: {readableValue(result)}</p>}
        </div>
      </div>
    </div>
  )
}

createRoot(document.body).render(<App />)

A Proxy, created with new Proxy(target, handlers), is composed of a target object as the first argument and any number of handlers as the second argument. These handlers, often referred to as traps, typically receive a reference to the target as their first argument. This design allows you to reuse the same set of handlers with different targets or for handling nested access scenarios.

In addition to the two most commonly used traps, namely get for property access and setfor property assignment, there exist various other traps that capture behaviors one might not typically consider when working with objects. It's worth noting that different traps come with specific rules that should be maintained, and achieving certain behaviors may not always be possible. However, when the aim is to maintain the regular behavior, you can utilize the Reflecthelper. For instance, to access a property on the target, you can use Reflect.get(target, prop) or simply target[prop] in this context.

To enable nested access to objects and properties, especially those that return objects themselves, you can employ a double-layered proxy approach. By encapsulating them within another proxy, you ensure that subsequent accesses are consistently handled through the proxy mechanism, allowing for seamless interaction.

In his revealing blog post titled Debugging Like A Pro-xy, Artem Zakharchenko demonstrates a powerful debugging technique. He leverages a Proxy to effectively trace and diagnose issues that might otherwise be elusive within external code. This method involves enveloping an input with a Proxy and strategically deploying console.trace when specific actions are triggered on the Proxy, enabling precise identification of the issue's location.

Observables

The concept of observables, stemming from the foundation of Proxies, offers a powerful mechanism for tracking and automating actions based on any read operation on an object. In the past, JavaScript included the Object.observe() standard, which has since been deprecated. In the era of Internet Explorer 11, observables were achieved through the use of getters and setters, eliminating the need for Proxies which weren't available.

Observables, at their core, enable the observation of any interaction with an object, allowing you to trigger actions as needed. They prove invaluable for updating states elsewhere in your codebase or persisting changes. Remarkably, observables seamlessly mimic regular JavaScript values, concealing their abstracted behaviors behind the scenes.

While you can construct observables manually, numerous libraries like MobX provide pre-built observables for your convenience. Being observable, any alteration can be intercepted with addons like mobx-persist-store. It's worth noting that observables constructed using Proxies can appear almost magical, but they may not always be as straightforward to work with as plain JavaScript objects. This complexity has contributed to the sustained popularity of alternatives like redux, which relies on immutable objects and often requires more verbose code.

Globs

Globs are a specific string syntax that provides a quick way to describe a subset of files within a location. For instance, glob('*.js') will retrieve all JavaScript files in the root directory. The asterisk * serves as a wildcard, matching any valid characters in a file name, excluding /, which denotes a folder. You can also use glob('**/*.tsx') to match TSX files in the root, even if they are nested within folders.

Despite being a well-known technique among developers, globs are still significantly underutilized. When designing a plugin and providing an interface to specify a subset of files, globs offer an ideal choice. The fast-glob library, for example, simplifies the process of searching for files based on glob patterns across any location. Conversely, checking if a single file descriptor matches a glob pattern can be accomplished using theminimatch library. Globs remain a versatile tool for managing and filtering files in various development scenarios.

import fastGlob from 'fast-glob'
import { minimatch } from 'minimatch'

await fastGlob(['*.js']) // ['index.js', 'styles.js']
minimatch('my-file.tsx', '*.tsx') // true

Enhancing Code Imports with Globs

One of the most practical applications of globs is in importing code from other files within ES Modules. While not strictly required, developers often organize their codebase by using separate files for each component and grouping related components within folders. In higher-level components, such as specific pages, multiple components from the same folder need to be imported, resulting in a separate import statement for each component. To address this challenge, developers have historically created an index file, likemarkup/basic/index.js, which imports components from individual files and exports them collectively. While effective, this approach requires manual maintenance. Globs offer a more automated alternative, streamlining the import process and reducing the need for manual index file management.

import { Button, Text, Heading, Link, Code } from 'markup/basic/*'

Why haven't glob imports become a standard feature in modern development? Much like observables, glob imports can appear almost magical, and many developers don't perceive the overhead of manually importing each file as a significant issue. Additionally, outside of plugin developers who often work closely with the file system, many developers remain unaware of the potential benefits of globs. Consequently, the absence of this feature often goes unnoticed and unreported.

From a technical standpoint, implementing glob imports is relatively straightforward. Plugins for build tools like webpack, esbuild, or babel can seamlessly transform glob imports into regular imports during the bundling process. However, a more significant challenge arises when integrating this feature into code editors like Visual Studio Code (VSCode). Editors need to understand the interfaces and types of variables being imported, and this information is crucial during development.

VSCode, in particular, presents a complex landscape for implementing glob imports. While it allows developers to create Language Server Extensions to integrate custom syntax, glob imports seem to operate at a lower level within the editor. This situation may require updates to the editor itself. The relevant VSCode implementation can be found on GitHub, specifically in the src/vs/loader.js file.

Forking VSCode and implementing glob imports offers a meaningful opportunity to flex 💪 your programming skills. VSCode is well-architected, and with a deep understanding of its abstractions, you might find it feasible to navigate the extensive source code and introduce this feature with relatively manageable effort. If you're looking for a substantial coding challenge that can enhance the development experience for many, this could be the perfect endeavor to showcase your expertise.

Better Async Error Handling Without Try-Catch

In contemporary development, many plugin interfaces return promises, enabling developers to write asynchronous code for improved performance. However, in practice, writing asynchronous code can become verbose, often involving adding await in front of each method call. Handling errors with promises typically demands wrapping every method in intricate try-catch blocks. While error handling is crucial, excessive try-catch statements can clutter code and reduce readability. Furthermore, unhandled errors may propagate, getting thrown somewhere else in the code, disconnecting them from the context in which they originated.

To tackle this challenge, I've developed a plugin named avait. This tool automatically resolves promises and returns processed error messages. If an error goes unhandled, a preconfigured handler steps in to manage the message, simplifying error management in asynchronous code. Additionally, it offers a convenient way to chain multiple promises, enhancing the overall flexibility and readability of asynchronous workflows.

import { it } from 'avait'
import { readFile } from 'fs/promises'

async function loadTextFile() {
  const { error, value } = await it(readFile('./text.txt', 'utf-8))
  if (error) return 'Failed to read file!'
  return value
}

await loadTextFile()

Async to Sync

One challenge that seemed insurmountable in the JavaScript language was transforming an existing asynchronous interface into a synchronous counterpart, allowing the same functionality as await without the need for it. With the release of prettier version 3, the codebase underwent a significant shift to a fully asynchronous implementation. This transition prompted developers, including myself, to migrate code that relied on the synchronous version of Prettier to asynchronous methods, right up to the initial function call.

Depending on a project's architecture or a plugin's design, this migration might necessitate the use of top-level await, a feature supported only in Node.js versions 14 and higher. While Prettier's timing aligned with Node.js 14's security support end date (September 11th of this year), rendering it an apt decision, some developers continue to seek a synchronous version of the tool for various reasons.

Surprisingly, despite the absence of build tools capable of transforming asynchronous code into a synchronous target, Prettier managed to provide a synchronous version, accessible as @prettier/sync. This version wraps the underlying asynchronous codebase, effectively converting it into a synchronous interface. Behind the scenes, it leverages Node.js worker_threads, a feature introduced in Node.js 10.

import { toSync } from 'avait'

// Synchronize an async module.
const { title } = toSync('node-fetch', ['default', 'json'])([
  ['https://dummyjson.com/products/1'], [],
])
// The same when using await:
const { title } = await (await fetch('https://dummyjson.com/products/1')).json()

The Workers employed in this process communicate exclusively through events by exchanging serializable data. This limitation means that sending entire functions to a worker for execution is not possible. Consequently, the toSync method in avait can only synchronize methods loaded from files on the system. However, synchronizing methods from a plugin is still achievable by specifying the plugin's name and the desired export, a practical solution for handling various use cases.

This post was revised with ChatGPT a Large Language Model.