A top-down view of Mcway Falls, with a pristine turquouse blue beach lagoon, surrounded on all sides by rocky cliffs.
📸

React context and algebraic effects

How React context inspires a more general API in programming languages.


Generally speaking, functions in most programming languages only allow explicit parameters. There’s no sense of a function being able to “use the context” in which it’s run in. But it’s a very powerful concept in React that I’d like to see a modern programming language experiment with.

If you’re unfamiliar with React Context, it’s a mechanism that lets you create generic abstractions that can use different implementations at run-time.

function Parent1() {
    // ...
    return (
        <LocalFileSystemProvider>
            <Child />
        </LocalFileSystemProvider>
    )
}

function Parent2() {
    // ...
    return (
        <RemoteFileSystemProvider>
            <Child />
        </RemoteFileSystemProvider>
    )
}

function Child() {
    // This will load whatever filesystem the component was mounted in
    const fs = useContext(FileSystemContext)
    // ...
}

In the example, the same child can be re-used but in contexts with different implementations of the filesystem.

You could technically replicate the same behavior by just using function arguments (<Child fs={remoteFs}/>), but I think this approach has a lot of benefits:

In the functional programming world, I think this construct is very similar to co-effects/algebraic effects, except most of the work I’ve seen around that tries to fit it into the type-system. This makes it so you can never call functions that expect a certain context in a place where they don’t have it (so Child in the above example would have a type error if it is mounted in a place that doesn’t have access to a FileSystemContext). Besides this being extremely complex to type-check, I think it’s also a mistake — tracking context in the type-system essentially turns them into function parameters with a different syntax. Having it dynamically resolve is more powerful.

Consider the following examples to see how powerful of a pattern this can be in non-React:

return ArenaAlloctatorProvider([
  Subsystem()
])

If that subsystem uses 3rd party libraries which also allocate memory, those will automatically use your arena allocator. You could only do this something like this today if you passed around the allocator as an argument everywhere. And if you were using a 3rd party data structure library which did not do that, you’re out of luck.

return NoFilesystemProvider([
  Subsystem()
])

Something like this would be practically impossible in most languages. Even in Javascript, to run insecure code that can’t modify the DOM or make fetch calls, you need a lot of infrastructure like Shadow Realms or quickjs-emscripten.

function div({ children: Children }) {
  const {min, max} = useLayoutContext()

  const parentSpace = (0, 0)
  for(const child of children) {
    const remainingMin = min - parentSpace
    const remainingMax = max - parentSpace

    const { usedSpace } =
      LayoutProvider(
        { min: remainingMin, max: remainingMax },
        [ Child() ]
      )

    parentSpace += usedSpace
  }

  return { usedSpace: parentSpace }
}

// The input and label are in the layout context of the div
div([
  input(),
  label("hi"),
])

That’s some very rough pseudo-code, and some serious details are missing (how does the Child ”return” how much space it used through the LayoutProvider?), but it’s illustrative in an abstract sense: you are able to create an API where user code does not have to worry about plumbing down a layout context to all its children; its done automatically.

In fact, this is exactly why HTML was created: to enable and visualize this idea of children being “inside” the layout context of parents.

In one sense, its like composable capability-based security. New runtimes like Deno allow top-level capability security (i.e. you write deno run --allow-net hello.ts to allow network access to the whole program), but they don’t let you specify capabilities for just a part of the program. Context is just that: you have fine-grained control over specifically what APIs each part of your program has. I think the WASM component model does something like this as well (your subcomponents only have access to the contexts you have access to) but I don’t know enough about it.

I’d really like this API in a general-purpose programming language. You can definitely build it as a library, but I think it’d be interesting as a first-class primitive.