I'd like to replicate the behavior of React Context in Nodejs but I'm struggling with it.
In React, by creating only one context, I can provide and consume different values in my components, depending on the value
given to the <Provider/>
. So the following works:
const MyContext = React.createContext(0);
const MyConsumer = () => {
return (
<MyContext.Consumer>
{value => {
return <div>{value}</div>
}}
</MyContext.Consumer>
)
}
const App = () =>
<React.Fragment>
<MyContext.Provider value={1}>
<MyConsumer/>
</MyContext.Provider>
<MyContext.Provider value={2}>
<MyConsumer/>
</MyContext.Provider>
</React.Fragment>;
ReactDOM.render(
<App/>,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="react"></div>
However I have no idea how to implement this in Nodejs. I've taken a look at the source code of React Context but it does not help much... Here is what I got so far:
// context.js
export const createContext = (defaultValue: number) => {
const context = {
value: defaultValue,
withContext: null,
useContext: null,
};
function withContext(value: number, callback: (...args: any[]) => any) {
context.value = value;
return callback;
}
function useContext() {
return context;
}
context.withContext = withContext;
context.useContext = useContext;
return context;
};
// functions.js
import { context } from "./index";
export function a() {
const result = context.useContext();
console.log(result);
}
export function b() {
const result = context.useContext();
console.log(result);
}
// index.js
import { createContext } from "./context";
import { a, b } from "./functions";
export const context = createContext(0);
const foo = context.withContext(1, a);
const bar = context.withContext(2, b);
console.log("foo", foo());
console.log("bar", bar());
Obviously, value
is overwritten and 2
is logged twice.
Any help will be much appreciated!
NodeJS is proposing a new built-in for doing exactly that: Asynchronous context tracking.
Thanks to @Emmanuel Meric de Bellefon for pointing this out.
If you only need it for synchronous code, you could do something relatively simple. All you need is to specify where the boundaries are.
In React, you do this with JSX
<Context.Provider value={2}>
<MyComponent />
</Context.Provider>
In this example, the value of Context
will be 2
for MyComponent
but outside of the bounds of <Context.Provider>
it will be whatever the value was before that.
If I were to translate that in vanilla JS, I would probably want it to look something like this:
const myFunctionWithContext = context.provider(2, myFunction)
myFunctionWithContext('an argument')
In this example, I would expect the value of context
to be 2
within myFunction
but outside of the bounds of context.provider()
it would be whatever value was set before.
At its most basic, this could be solved by a global object
// we define a "context"
globalThis.context = 'initial value'
function a() {
// we can access the context
const currentValue = globalThis.context
console.log(`context value in a: ${currentValue}`)
// we can modify the context for the "children"
globalThis.context = 'value from a'
b()
// we undo the modification to restore the context
globalThis.context = currentValue
}
function b() {
console.log(`context value in b: ${globalThis.context}`)
}
a()
Now we know that it's never wise to pollute the global scope globalThis
or window
. So we could use a Symbol instead, to make sure there can't be any naming conflict:
const context = Symbol()
globalThis[context] = 'initial value'
function a() {
console.log(`context value in a: ${globalThis[context]}`)
}
a()
However, even though this solution will never cause a conflict with the global scope, it's still not ideal, and doesn't scale well for multiple contexts. So let's make a "context factory" module:
// in createContext.js
const contextMap = new Map() // all of the declared contexts, one per `createContext` call
/* export default */ function createContext(value) {
const key = Symbol('context') // even though we name them the same, Symbols can never conflict
contextMap.set(key, value)
function provider(value, callback) {
const old = contextMap.get(key)
contextMap.set(key, value)
callback()
contextMap.set(key, old)
}
function consumer() {
return contextMap.get(key)
}
return {
provider,
consumer,
}
}
// in index.js
const contextOne = createContext('initial value')
const contextTwo = createContext('other context') // we can create multiple contexts without conflicts
function a() {
console.log(`value in a: ${contextOne.consumer()}`)
contextOne.provider('value from a', b)
console.log(`value in a: ${contextOne.consumer()}`)
}
function b() {
console.log(`value in b: ${contextOne.consumer()}`)
console.log(`value in b: ${contextTwo.consumer()}`)
}
a()
Now, as long as you're only using this for synchronous code, this works by simply overriding a value before a callback and reseting it after (in provider
).
If you want to structure your code like you would in react, here's what it would look like with a few separate modules:
// in createContext.js
const contextMap = new Map()
/* export default */ function createContext(value) {
const key = Symbol('context')
contextMap.set(key, value)
return {
provider(value, callback) {
const old = contextMap.get(key)
contextMap.set(key, value)
callback()
contextMap.set(key, old)
},
consumer() {
return contextMap.get(key)
}
}
}
// in myContext.js
/* import createContext from './createContext.js' */
const myContext = createContext('initial value')
/* export */ const provider = myContext.provider
/* export */ const consumer = myContext.consumer
// in a.js
/* import { provider, consumer } from './myContext.js' */
/* import b from './b.js' */
/* export default */ function a() {
console.log(`value in a: ${consumer()}`)
provider('value from a', b)
console.log(`value in a: ${consumer()}`)
}
// in b.js
/* import { consumer } from './myContext.js' */
/* export default */ function b() {
console.log(`value in b: ${consumer()}`)
}
// in index.js
/* import a from './a.js' */
a()
The solution proposed above would not work if b()
was an async function, because as soon as b
returns, the context value is reset to its value in a()
(that's how provider
works). For example:
const contextMap = new Map()
function createContext(value) {
const key = Symbol('context')
contextMap.set(key, value)
function provider(value, callback) {
const old = contextMap.get(key)
contextMap.set(key, value)
callback()
contextMap.set(key, old)
}
function consumer() {
return contextMap.get(key)
}
return {
provider,
consumer
}
}
const { provider, consumer } = createContext('initial value')
function a() {
console.log(`value in a: ${consumer()}`)
provider('value from a', b)
console.log(`value in a: ${consumer()}`)
}
async function b() {
await new Promise(resolve => setTimeout(resolve, 1000))
console.log(`value in b: ${consumer()}`) // we want this to log 'value from a', but it logs 'initial value'
}
a()
So far, I don't really see how to manage the issue of async functions properly, but I bet it could be done with the use of Symbol
, this
and Proxy
.
this
to pass context
While developing a solution for synchronous code, we've seen that we can "afford" to add properties to an object that "isn't ours" as long as we're using Symbol
keys to do so (like we did on globalThis
in the first example). We also know that functions are always called with an implicit this
argument that is either
globalThis
),obj.func()
, within func
, this
will be obj
).bind
, .call
or .apply
)In addition, javascript lets us define a Proxy to be the interface between an object and whatever script uses that object. Within a Proxy
we can define a set of traps that will each handle a specific way in which our object is used. The one that is interesting for our issue is apply which traps function calls and gives us access to the this
that the function will be called with.
Knowing this, we can "augment" the this
of our function called with a context provider context.provider(value, myFunction)
with a Symbol referring to our context:
{
apply: (target, thisArg = {}, argumentsList) => {
const scope = Object.assign({}, thisArg, {[id]: key}) // augment `this`
return Reflect.apply(target, scope, argumentsList) // call function
}
}
Reflect
will call the function target
with this
set to scope
and the arguments from argumentsList
As long as what we "store" in this
allows us to get the "current" value of the scope (the value where context.provider()
was called) then we should be able to access this value from within myFunction
and we don't need to set/reset a unique object like we did for the synchronous solution.
Putting it all together, here's an initial attempt at a asynchronous solution for a react-like context. However, unlike with the prototype chain, this
is not inherited automatically when a function is called from within another function. Because of this the context in the following solution only survives 1 level of function calls:
function createContext(initial) {
const id = Symbol()
function provider(value, callback) {
return new Proxy(callback, {
apply: (target, thisArg, argumentsList) => {
const scope = Object.assign({}, thisArg, {[id]: value})
return Reflect.apply(target, scope, argumentsList)
}
})
}
function consumer(scope = {}) {
return id in scope ? scope[id] : initial
}
return {
provider,
consumer,
}
}
const myContext = createContext('initial value')
function a() {
console.log(`value in a: ${myContext.consumer(this)}`)
const bWithContext = myContext.provider('value from a', b)
bWithContext()
const cWithContext = myContext.provider('value from a', c)
cWithContext()
console.log(`value in a: ${myContext.consumer(this)}`)
}
function b() {
console.log(`value in b: ${myContext.consumer(this)}`)
}
async function c() {
await new Promise(resolve => setTimeout(resolve, 200))
console.log(`value in c: ${myContext.consumer(this)}`) // works in async!
b() // logs 'initial value', should log 'value from a' (the same as "value in c")
}
a()
A potential solution for the context to survive a function call within another function call could be to have to explicitly forward the context to any function call (which could quickly become cumbersome). From the example above, c()
would change to:
async function c() {
await new Promise(resolve => setTimeout(resolve, 200))
console.log(`value in c: ${myContext.consumer(this)}`)
const bWithContext = myContext.forward(this, b)
bWithContext() // logs 'value from a'
}
where myContext.forward
is just a consumer
to get the value and directly afterwards a provider
to pass it along:
function forward(scope, callback) {
const value = consumer(scope)
return provider(value, callback)
}
Adding this to our previous solution:
function createContext(initial) {
const id = Symbol()
function provider(value, callback) {
return new Proxy(callback, {
apply: (target, thisArg, argumentsList) => {
const scope = Object.assign({}, thisArg, {[id]: value})
return Reflect.apply(target, scope, argumentsList)
}
})
}
function consumer(scope = {}) {
return id in scope ? scope[id] : initial
}
function forward(scope, callback) {
const value = consumer(scope)
return provider(value, callback)
}
return {
provider,
consumer,
forward,
}
}
const myContext = createContext('initial value')
function a() {
console.log(`value in a: ${myContext.consumer(this)}`)
const bWithContext = myContext.provider('value from a', b)
bWithContext()
const cWithContext = myContext.provider('value from a', c)
cWithContext()
console.log(`value in a: ${myContext.consumer(this)}`)
}
function b() {
console.log(`value in b: ${myContext.consumer(this)}`)
}
async function c() {
await new Promise(resolve => setTimeout(resolve, 200))
console.log(`value in c: ${myContext.consumer(this)}`)
const bWithContext = myContext.forward(this, b)
bWithContext()
}
a()
Now I'm stuck... I'm open to ideas!
Your goal of "replicating React's Context in NodeJS" is slightly ambiguous. From the React docs:
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
There are no component trees in NodeJS. The closest analogy that I could think of (based also on your example) was a call stack. Additionally, React's Context also causes a re-render of the tree if the value changes. I have no idea what that would mean in NodeJS, so I'll happily ignore this aspect.
Thus I will assume that you are essentially looking for a way to make a value accessible anywhere in the call stack without having to pass it down the stack as an argument from function to function.
I propose you use one of the so-called continuation-local storage libs for NodeJS to achieve this. They use a pattern that is a little different from what you were trying to do, but it might be just fine.
My favourite has been CLS Hooked (no affiliation). It taps into NodeJS's async_hooks system to preserve the provided context even if there are asynchronous calls in the stack. Last published 4 years ago it still works as expected.
I rewrote your example using CLS Hooked, although I'd argue that it's not the nicest / most intuitive way to use it. I also added an extra function call to demonstrate that it's possible to override values (i.e. create sort of child contexts). Finally, there's one noticeable difference - the context must now have an ID. If you wish to stick with this React Contexty pattern, you'll probably have to make peace with it.
// context.js
import cls from "cls-hooked";
export const createContext = (contextID, defaultValue) => {
const ns = cls.createNamespace(contextID);
return {
provide(value, callback) {
return () =>
ns.run(() => {
ns.set("value", value);
callback();
});
},
useContext() {
return ns.active ? ns.get("value") : defaultValue;
}
};
};
// my-context.js
// your example had a circular dependency problem
// the context has to be created in a separate file
import { createContext } from "./context";
export const context = createContext("my-context", 0);
// zz.js
import { context } from "./my-context";
export const zz = function () {
console.log("zz", context.useContext());
};
// functions.js
import { context } from "./my-context";
import { zz } from "./zz";
export const a = function () {
const zzz = context.provide("AAA", zz);
zzz();
const result = context.useContext();
console.log("a", result);
};
export const b = function () {
const zzz = context.provide("BBB", zz);
zzz();
const result = context.useContext();
console.log("b", result);
};
// index.js
import { context } from "./c";
import { a, b } from "./functions";
const foo = context.provide(1, a);
const bar = context.provide(2, b);
console.log("default value", context.useContext());
foo();
bar();
Running node index
logs:
default value 0
zz AAA
a 1
zz BBB
b 2
This would also work if there were all sorts of asynchronous calls happening in your stack.
My approach is a little different. I wasn't trying to replicate React's Context, which also has a limitation in that it is always bound to a single value.
// cls.ts
import cls from "cls-hooked";
export class CLS {
constructor(private readonly NS_ID: string) {}
run<T>(op: () => T): T {
return (cls.getNamespace(this.NS_ID) || cls.createNamespace(this.NS_ID)).runAndReturn(op);
}
set<T>(key: string, value: T): T {
const ns = cls.getNamespace(this.NS_ID);
if (ns && ns.active) {
return ns.set(key, value);
}
}
get(key: string): any {
const ns = cls.getNamespace(this.NS_ID);
if (ns && ns.active) {
return ns.get(key);
}
}
}
// operations-cls.ts
import { CLS } from "./cls";
export const operationsCLS = new CLS("operations");
// consumer.ts
import { operationsCLS } from "./operations-cls";
export const consumer = () => {
console.log(operationsCLS.get("some-value")); // logs 123
};
// app.ts
import { operationsCLS } from "./operations-cls";
import { consumer } from "./consumer";
cls.run(async () => {
cls.set("some-value", 123);
consumer();
});
I prefer to view CLS as magic, as it's always worked fine without my intervention, so can't comment much here, sorry :]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With