Skip to content

Instantly share code, notes, and snippets.

@alxgmpr
Last active March 25, 2022 19:36
Show Gist options
  • Save alxgmpr/d5fa60443453b49f286a08147468cba0 to your computer and use it in GitHub Desktop.
Save alxgmpr/d5fa60443453b49f286a08147468cba0 to your computer and use it in GitHub Desktop.

Enforcing Singletons

Lateley at work, I've become invested in making sure that we only have one of things. In modern development, we are blessed with patterns that don't require instantiating expensive things, or not having to worry about creating them at all. But when working with service class instances which talk over the wire, it becomes imperative to enforce expensive operations only being done once, and moreover ensuring that multiple consumers of the same singleton don't duplicate requests.

Simple Window Instance

I've found that the easiest way to ensure instantiation only occurs once (in browser contexts) is to use the window.

function getOrCreateExpensiveService(): ExpensiveService {
  window.ExpensiveService ??= new ExpensiveService()
  return window.ExpensiveService
}

This pattern is one of my favorites for the conciseness, although it does sacrifice some readability. The 'nullish assignment' operator is doing the 'get-or-create' heavy lifting in this case, but it can be stated in other more verbose ways:

window.ExpensiveService ??= new ExpensiveService()

// or

window.ExpensiveService = window.ExpensiveService ?? new ExpensiveService()

// or if we don't care about distinction between falsy values:

window.ExpensiveService ||= new ExpensiveService()

// expanded

window.ExpensiveService = window.ExpensiveService || new ExpensiveService()

// we can also use a ternary

window.ExpensiveService = typeof window.ExpensiveService === 'undefined' ? new ExpensiveService() : window.ExpensiveService

// or be brutally explicit

if (!!window.ExpensiveService) {
  return window.ExpensiveService
} else {
  const newService = new ExpensiveService
  window.ExpensiveService = newService
  return newService
}

Regardless of exact implementation, the point is twofold:

  1. let the browser window handle storing a global instance
  2. leverage a factory function pattern to control instantiation of expensive things

In React Components

When running in a context that you as a developer can control (i.e. writing your own app, not running in someone else's), React can make a similar pattern leveraging useRef. This was called out by Dan Abramov here. This may not be a true singleton pattern, but is a method for ensuring React doesn't recreate expensive things on each render.

const Button: FC = () => {
  const serviceRef = useRef(null)
  
  function getService(): ExpensiveService {
    if (serviceRef.current !== null) return serviceRef.current
    const newService = new ExpensiveService()
    serviceRef.current = newService
    return newService
  }
  
  // anything that needs the service can call getService to retrieve the same instance
  
  return <button>My Button</button>
}

Note the similarities between the getService method we wrote here and the one above. It's another get-or-create.

You could hoist this logic higher into a common hook like useExpensiveService.

This will break down, however, if you are rendering individual components (like with a component library or rendering React to a non-React app). In such a case it is beneficial to combine both methods or only use the window method at all.

Deduplicating Requests

Having a single instance of a class doesn't solve all our problems. What if our instantiated class has the power to make network calls?

One way would be to keep the request promise on our class itself:

class ExpensiveClass {
  private _responsePromise: Promise<Response>
  
  // ...
}

And use a get-or-create pattern against it:

public async goToServerForSomething(): Promise<Something> {
  this._responsePromise ??= fetch(...)
  
  return this._responsePromise.then((res) => res.clone()).then((clone) => clone.json())
}

Important: make sure to use response.clone() because Response bodies can only be read once! So subsequent attempts to .json() the same response promise will fail. By using the clone, we have a copy of the body for every consumer that wants to .json()-ify the response. This is the same for .text() and .blob(). See response.bodyUsed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment