First, some context
A context API is the foundation for re-usable complex libraries and components, and we don’t yet have a great context API for TypeScript client and server development (or at least that I’m aware of).
Context allows code to access variables that aren’t global and also aren’t passed in as standard function arguments.
Without context, code inevitably ends up as a mix of global variables, hardcoded dependencies between components, and awkward APIs to tunnel user IDs and expose functionality for testing.
Although inelegant, this mix isn’t a huge problem for code used within a single product. But for an ecosystem of reusable libraries this coding style becomes unmanageable.
On top of the API, for context to provide value we need standard definitions for the types of the most common context variables.
What is a context API?
(reasonable people may have other definitions - this just for the purposes of this post)
A Context API allows code to access variables that aren’t global and also aren’t passed in as a standard function argument.
These variables are part of a logical “context” that can be scoped to:
A server request
A React component tree (or other hierarchical representation of client UI, or
An initial process and spawned subprocess
A common Context API pattern is dependency injection during initialization of objects or classes, where constructor arguments, class fields, and initial setter values are provided from the context. Example from Google Guice:
@Inject
public void setEmailCommunicator(
@Named("EmailComms") CommunicationMode emailComms) {
this.emailComms = emailComms;
}
Another pattern is using a functional API to retrieve a value from the context. An example from React:
import { useContext } from 'react';
function MyComponent() {
const theme = useContext(ThemeContext);
// ...
Is this just dependency injection?
Both of these use cases I think technically can be classified as dependency injection, but DI has strong associations for people and I’m using “context” to avoid preconceptions.
Specifically, I don’t think most people think of React Context as DI.
There is a lot of benefit to using the learnings from server dependency injection in client code, and as a bonus doing so in a way that allows sharing code between client and server.
If you’re familiar with dependency injection, the notes below may be a rehash. But if you want a TypeScript dependency injection library, stick around for the next post where I’ll cover Providers
.
Use cases for a context API
The examples I find most compelling:
Use Case 1: Logging, including User ID
Library code frequently has trouble logging effectively and ends up being a black box in production, because:
There aren’t standard logging libraries to use.
Even when the library chooses a specific logger, these logs don’t have access to the current user ID. The user ID is required for product logging and extremely valuable for reliability and performance logging.
With context, you can define a standard, pluggable logging API and have implementations retrieve the ID of the currently logged in user from the context.
Use Case 2: Flags
A best practice is to roll out significant user changes behind flags. However library changes are rarely flag guarded — you need to upgrade the whole library and roll back your whole release if there are issues.
With context and a standard flag API, these changes can be managed cleanly by having an interop version that supports old and new library behavior.
Use Case 3: Shared client / server code
If your app uses the same programming language on client and server (e.g. React and Node.js) and you’ve tried to share code, you’ve likely found it surprisingly complicated to share functionality.
For an async method (such as hasUserLikedBook()
) that you want to be available both on client and server, it needs to make a client→server request from the client, while calling directly into the database on the server.
Context allows you to provide different implementations on client and server for the same functionality.
More generally: Why Context?
Context APIs often arise out of practical problems developers are facing, and that's why I started with real-world use cases.
However, also wanted to generalize as to why context is important... This is mostly just my take on the benefits of dependency injection.
Without context, you always end up with global state:
It’s a law of nature - you will hit a piece of code that doesn’t have access to the current user, and it will be prohibitive to tunnel through all the intermediate layers
And even when you can tunnel, the intermediate layers get polluted with variables they don’t directly access, making it harder to reason about functionality and to test
Global state is often a reasonable approach, however it frequently causes correctness issues with mutable state such as the current user or flags, it’s difficult for testing, and it feels like you’re doing it wrong
If you don’t have context, you end up with awkward component APIs and unwanted dependencies:
Higher-level libraries need to trigger complex behavior and provide customization - they’ll want to save a preference, providing customization to clients for button look and feel, or enable using an analytics library requested by a specific customer
To support these needs, libraries often end up with an assortment of one-off customization hooks and hard-coded dependencies. Or alternately the libraries don’t extend to reasonable customer needs and the customers stop using the library and roll their own version.
Context provides a foundation for re-usable complex components
Things you might find in a context
ID and credentials for the currently logged in user
Pluggable implementations for core services: logging, auth, data storage, file storage, etc
UI Theme: Scalar values (colors, fonts) as well as full component implementations (e.g. passing in a specific Button component)
Current country and language
Configuration for low-level services: Facebook and Google auth, notifications, etc
App-wide configuration: Staging vs. prod, product name and logo, etc
Goals for a TypeScript context API
What I'd like to see in a context API for TypeScript:
Ability to share code across client & server: in React components, Node.js server code, and non-React client code (e.g. background process triggered by a Geofence notification)
TypeScript-flavored API, and specifically using types wherever possible
Standard definitions of the types for core dependencies, including ID and credentials for current user, logging, flags, and the current locale
Bonus: Can be used in place of React Context to avoid deeply nested provider trees
If this sounds interesting, stick around for the next post where I'll outline Providers
, a halfway decent implementation of a context API in the NPE Toolkit.
About these posts
Thanks for taking an interest in what I have to say! 🙏
Although the tools for building apps have never been better, there aren’t commonly used abstractions for users and auth, logging, flags, data access, storage, and theming, and this feels like a hole that needs to be filled.
In my day job I’m a software engineer at Snowflake (starting in a couple of weeks). Previously I worked at Meta as part of their New Product Experimentation group, and before that I was at Google, eBay, Ask Jeeves, and a bunch of startups that you probably never heard of.
The “UI” in “uidude” refers to my being a product and frontend infrastructure engineer more than a backend / database dude (although I do manage to find ways to meddle at all layers of the stack). And I’m definitely not a UI designer, as you’ll see if I post any UI that I've built. :)