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.
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:
- let the browser
window
handle storing a global instance - leverage a factory function pattern to control instantiation of expensive things
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.
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
.