# Providers, a context API for TypeScript

The [previous post](https://uidude.dev/first-some-context) discussed what context APIs are and why they are important.

This post outlines [Providers](https://github.com/npe-toolkit/npe-toolkit/tree/main/lib/core/providers), a halfway decent implementation of a TypeScript Context API we built for the NPE Toolkit.

## Recap: [Goals](https://uidude.dev/first-some-context#heading-goals-for-a-typescript-context-api) for a TypeScript context API

* Allow sharing 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 the current user, logging, flags, and the current locale
    
* Bonus: Can be used in place of [React Context](https://react.dev/learn/passing-data-deeply-with-context) to avoid deeply nested provider trees
    

## Overview

The core of the Providers API is straightforward:

* A key is defined for each provided value (a “ProviderKey”). ProviderKeys are associated with a TypeScript type (for type-safe usage).
    
* At startup you configure which providers are used for these keys
    
* At runtime call `use(ProviderKey)` to get a provided value
    

### Example code to use Providers

The code that most developers will interact with:

```typescript
// Getting a provided value
const log = use(LogApiKey); // <- LogApiKey is a `ProviderKey`
...
log({name: 'UserLike'});

// Configuring Providers on the client
<Scope providers={[/* list of providers */]}>
  ...
</Scope>

// Configuring Providers on the server
addProviderScope([/* list of providers */]); // <- In request initialization code
```

### Example code defining Providers

These calls are only needed if you’re defining new providers:

```typescript
// Creating a `ProviderKey`
type Theme = {primary: string, secondary: string};
export const ThemeKey = providerKeyFor<Theme>();

// Defining a provider by value
const MyTheme = {primary: 'red', secondary: 'blue'};
providesValue(ThemeKey, MyTheme); // <- `MyTheme` is of type `Theme`

// Defining a provider by function that returns value
function ConsoleLogger() {
  const flags = useFlags();
  enabled = flags.enabled('ConsoleLogger');

  return (event: LogEvent) => {
    if (enabled) {
      console.log(event);
    }
  };
}
provides(LogApiKey, ConsoleLogger);
```

Providers (`MyTheme` and `ConsoleLogger` above) are used in an app by configuring at startup, using `<Scope providers=...>` on the client, and `addProviderScope(...)` on the server.

## Sharing code between client and server

A challenge in sharing code used in React apps between client and server is that a large percentage of useful client code uses React context. Since React context isn’t available in server request handlers, the code can’t be easily shared.

With providers, all access to context is via `use()`, with a React Context client implementation and a request-scoped server implementation using `cls-hooked`.

Having a shared API layer on top of these underlying primitives makes it possible to share code that uses context between client and server.

## Changing values at runtime

The `useScope()` API gives direct access to the context so that you can change values at runtime. This is used, for example, when a user logs in or logs out. Example code:

```typescript
const scope = useScope();

function onUserChange(user: newUser) {
  scope.provide(UserKey, user);
}
```

On the client, calls sites that `use()` a provided value that changed will be re-rendered. On the server, values don’t change during the processing of a single request.

## Providers with UI

Some client-only providers need an associated React component tree to provide UI..To support this, you can provide an inner function from the component using `useScope().`

An example AlertDialog that needs a component at the top level of the React Component tree:

```typescript
function AlertDialog() {
  const scope = useScope();  // <- Gets a 
  const visible = React.useState(false);

  function showAlert(text: string) {
    ...
    setVisible(true);
  }
  
  scope.provideValue(AlertApiKey, showAlert);

  return (
    <>
      {visible && <View>
        ...
      </View>
    </>
}
```

To configure, the app adds `<AlertDialog>` as a child of the top level`<Scope>.` To show an alert, the app calls `use(AlertApiKey); ... alert('We have a problem');`

## Other implementation notes

**ProviderKeys**

* `ProviderKey` uses a JavaScript symbol to uniquely identify a providable value.
    
* When defining a key using `providerKeyFor()`, you can pass in a default provider and a string `name` to use in debugging.
    

**Providing values**

* Calling `providesValue(key, value)` or `provides(key, fnReturningValue)` marks the object or function passed in as a provider by adding a symbol to it.
    
* Objects or functions marked as providers are not a different TypeScript type than the original value. This was a tradeoff — by using the existing value (as opposed to a wrapper), we avoided a level of indirection which makes it easier for users to find the implementation of a provider, with the downside that you don’t get static type checks for some calls.
    

**Helper functions**

* A common pattern that emerged when using Providers was to wrap the call to `use(ProviderKey)` with a no-arg function, so clients call `useTheme()` instead of `use(ThemeApiKey)`. This was just for developer ergonomics — these calls are functionally identical.
    
* Additionally, we often found ourselves creating helper functions for API variants, so that we didn’t create a new ProviderKey. An example: we wrapped `use(LogApiKey)` with `useLogEvent(name: string)` for a simple logging API.
    

**Server Providers**

* Providers on the server use `cls-hooked` library to share a thread-level context. This provider context is propagated to threads spawned by the server request.
    
* The rules of hooks don’t apply to server code, so `use()` can be called by any function at any layer of request processing.
    

## Status of Providers

[Providers](https://github.com/npe-toolkit/npe-toolkit/tree/main/lib/core/providers) we think will be generally useful, but it hasn’t been exposed as a standalone NPM package because it’s a work in progress. It likely still has some bugs and we may iterate on the API surface.

If you want to use it, feel free to copy!

If you think this should be packaged up and want to contribute, or have any comments or questions, get in touch with me at [uidude@innate.net](mailto:uidude@innate.net).

## 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 week). 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 :)
