Optionally Shared Context in React Components

By admin
Right, here’s a weird one. I know what many of you are thinking:


Alex
PERSON

, why the heck are you writing about React?

The answer is, I just solved an issue for someone and they said to write a blog post. So fine, I’m writing a blog post.

Shared React Context

So my friend had a problem:

I’ve not dealt with this before: I’m helping someone who has a React component that is reused across a codebase. It pulls variables from a

Context
ORG

to be used in a few functions. BUT, if it’s in a different part of the app, those variables should come from a different

Context
ORG

. The component has a prop that tells you which context it should pull from, but you can’t put the

useContext
ORG

hooks in an if statement

When I see an issue like this my gut instinct is to point out that in vue you can have optionally reactive data and/or throw this type of thing into a computed value and move on with your day. (Edit: I have been informed that the react linter will pitch a fit about this, but _

useContext
PERSON

_ can actually be accessed conditionally) But I bit my tongue, and made a minimal reproduction with a solution.

Shared Context – Normal Brain Version

import { createContext ,

useContext
ORG

} from ‘react’ ; import * as ReactDOM from ‘react-dom’ const App1Context = createContext ( null ) ; const App2Context = createContext ( null ) ; const MyComponent = ( ) => { const app1 =

useContext
PERSON

( App1Context ) ; const app2 =

useContext
PERSON

( App2Context ) ; return < h1 > { app1 ? app1 : app2 } < / h1 > ; } ; const Apps = [ ] ; Apps . push ( ( ) => { return ( < App1Context . Provider value = "

Hello App 1
WORK_OF_ART

!" > < MyComponent / > < / App1Context . Provider > ) ; } ) ; Apps . push ( ( ) => { return ( < App2Context . Provider value = "

Hello App 2
WORK_OF_ART

!" > < MyComponent / > < / App2Context . Provider > ) ; } ) ; Apps . forEach ( ( Component ) => { const mount = document . createElement ( "div" ) ; document . body . append ( mount ) ; ReactDOM . createRoot ( mount ) . render ( < Component / > ) ; } ) ;

Here’s a working demo

The way this works is by requesting both context values. If the

1st
ORDINAL

one exists, we use that. if the

second
ORDINAL

one exists, then we use that instead.


Bada
PERSON

-bing,

Bada
PERSON

-boom, solved.

Mostly solved

So, someone commented that really only works if you always want it to default to using the

first
ORDINAL

context, but what if you wanted to use the

2nd
ORDINAL

context

first
ORDINAL

?

Well, you’d need to be able to allow to specify the context order that we pick from, so let’s make a slightly more complex version.

Shared Context – Big Brain Version

import { createContext ,

useContext
ORG

} from ‘react’ ; import * as ReactDOM from ‘react-dom’ const App1Context = createContext ( null ) ; const App2Context = createContext ( null ) ; const MyComponent = ( { context } ) => { const chooseContext = { app1 :

useContext
ORG

( App1Context ) , app2 :

useContext
PERSON

( App2Context ) } ; if ( ! context ) { context = [ "app1" , "app2" ] ; } if (

Array
PRODUCT

. isArray ( context ) ) { for ( const app of context ) { if ( chooseContext [ app ] ) { return < h1 > { chooseContext [ app ] } < / h1 > ; } } } if ( typeof context === "string" ) { if ( chooseContext [ context ] ) { return < h1 > { chooseContext [ context ] } < / h1 > ; } } return < h1 > YOU PROVIDED NOTHING ! ! < / h1 > ; } ; const Apps = [ ] ; Apps . push ( ( ) => { return ( < App1Context . Provider value = "

Hello App 1
WORK_OF_ART

!" > < MyComponent / > < / App1Context . Provider > ) ; } ) ; Apps . push ( ( ) => { return ( < App2Context . Provider value = "

Hello App 2
WORK_OF_ART

!" > < MyComponent / > < / App2Context . Provider > ) ; } ) ; Apps . push ( ( ) => { return < MyComponent / > ; } ) ; Apps . push ( ( ) => { return ( < App1Context . Provider value = "

Hello App 4
WORK_OF_ART

!" > < App2Context . Provider value = "This will not display." > < MyComponent / > < / App2Context . Provider > < / App1Context . Provider > ) ; } ) ; Apps . push ( ( ) => { return ( < App1Context . Provider value = "This will not display." > < App2Context . Provider value = "

Hello App 5
WORK_OF_ART

!" > < MyComponent context = "app2" / > < / App2Context . Provider > < / App1Context . Provider > ) ; } ) ; Apps . forEach ( ( Component ) => { const mount = document . createElement ( "div" ) ; document . body . append ( mount ) ; ReactDOM . createRoot ( mount ) . render ( < Component / > ) ; } ) ;

Working demo here

This version allows you to pass a context prop that can be an array of strings or just a string, and specify which context order you want, or which context specifically you want.

People said this looked good.


Bada
PERSON

-bing,

Bada
PERSON

-boom, done.

I was hungry so I started to go make lunch. My brain however started to do loop-de-loops in my head, because my brain LOVES patterns. Like way too much. But it saw one. it saw something that could be refactored into an easier use case.

Fine, brain. Let’s go.


SharedContext – Galaxy Brain Mode

ORG

Let’s start with the generic thing we can use to make this better.

import {

useContext
ORG

} from ‘react’ ; export class

SharedContext
PERSON

{ static #registry = new Map ( ) ; static register ( sharedName ,

Context
ORG

) { if ( !

SharedContext
PERSON

. #registry . has ( sharedName ) ) {

SharedContext
ORG

. #registry . set ( sharedName , new Map ( ) ) ; } const ctx =

SharedContext
WORK_OF_ART

. get ( sharedName ) ; const identifier = Symbol ( sharedName + ctx . size ) ; ctx . set ( identifier ,

Context
ORG

) ; return identifier ; } static get ( sharedName ) { return

SharedContext
ORG

. #registry . get ( sharedName ) ; } static use ( sharedName , fallback , context ) { const ctxMap =

SharedContext
PERSON

. get ( sharedName ) ; if ( ! ctxMap ) { throw Error ( ` no such

SharedContext
ORG

:

${
PERSON

sharedName } ` ) ; } const ctx = new Map ( ) ; ctxMap . forEach ( (

Context
ORG

, identifier ) => { ctx . set ( identifier ,

useContext
ORG

( Context ) ) ; } ) ; if ( ! context ) { context = [ … ctx . keys ( ) ] ; } if (

Array
PRODUCT

. isArray ( context ) ) { for ( const id of context ) { if ( ctx . get ( id ) ) { return ctx . get ( id ) ; } } } if ( typeof context === "symbol" ) { if ( ctx . has ( context ) ) { return ctx . get ( context ) ; } } return fallback ; } }

This code gives us a generic way of creating “Shared Context” states that have a default order. You use SharedContext.register to register a context to a shared namespace, and it will return a unique Identifier Symbol that you can use to specify the context again when you want to use it. Otherwise it’ll use the ones that are registered in the order they are registered. To use the Shared context you call the

SharedContext.use
ORG

hook, where you give it the shared namespace, and a fallback value as a default. you can also specify the specific context, or order of the context you would like to use.

The code to use this looks something like this:

import { createContext } from ‘react’ ; import * as ReactDOM from ‘react-dom’ ; import {

SharedContext
WORK_OF_ART

} from ‘./shared-context.js’ ; const App1Context = createContext ( null ) ; const App2Context = createContext ( null ) ; const App1ContextId =

SharedContext
PERSON

. register ( "App" , App1Context ) ; const App2ContextId =

SharedContext
PERSON

. register ( "App" , App2Context ) ; const App2ContextIdAlt =

SharedContext
PERSON

. register ( "Alt" , App2Context ) ; const App1ContextIdAlt =

SharedContext
PERSON

. register ( "Alt" , App1Context ) ; const MyComponent = ( { context } ) => { const ctx =

SharedContext
WORK_OF_ART

. use ( "App" , "

YOU PROVIDED NOTHING!
WORK_OF_ART

!" , context ) ; return < h1 > { ctx } < / h1 > ; } ; const

AltComponent
PERSON

= ( { context } ) => { const ctx =

SharedContext
WORK_OF_ART

. use ( "Alt" , "

YOU PROVIDED NOTHING!
WORK_OF_ART

!" , context ) ; return < h1 > { ctx } < / h1 > ; } ; const Apps = [ ] ; Apps . push ( ( ) => { return ( < App1Context . Provider value = "

Hello App 1
WORK_OF_ART

!" > < MyComponent / > < / App1Context . Provider > ) ; } ) ; Apps . push ( ( ) => { return ( < App2Context . Provider value = "

Hello App 2
WORK_OF_ART

!" > < MyComponent / > < / App2Context . Provider > ) ; } ) ; Apps . push ( ( ) => { return < MyComponent / > ; } ) ; Apps . push ( ( ) => { return ( < App1Context . Provider value = "

Hello App 4
WORK_OF_ART

!" > < App2Context . Provider value = "This will not display." > < MyComponent / > < / App2Context . Provider > < / App1Context . Provider > ) ; } ) ; Apps . push ( ( ) => { return ( < App1Context . Provider value = "This will not display." > < App2Context . Provider value = "

Hello App 5
WORK_OF_ART

!" > < MyComponent context = { App2ContextId } / > < / App2Context . Provider > < / App1Context . Provider > ) ; } ) ; Apps . push ( ( ) => { return < hr / > ; } ) ; Apps . push ( ( ) => { return ( < App1Context . Provider value = "

Hello Alt 1
WORK_OF_ART

!" > < AltComponent / > < / App1Context . Provider > ) ; } ) ; Apps . push ( ( ) => { return ( < App2Context . Provider value = "

Hello Alt 2
WORK_OF_ART

!" > < AltComponent / > < / App2Context . Provider > ) ; } ) ; Apps . push ( ( ) => { return < MyComponent / > ; } ) ; Apps . push ( ( ) => { return ( < App1Context . Provider value = "This will not display." > < App2Context . Provider value = "

Hello Alt 4
WORK_OF_ART

!" > < AltComponent / > < / App2Context . Provider > < / App1Context . Provider > ) ; } ) ; Apps . push ( ( ) => { return ( < App1Context . Provider value = "

Hello Alt 5
WORK_OF_ART

!" > < App2Context . Provider value = "This will not display." > < AltComponent context = { App1ContextIdAlt } / > < / App2Context . Provider > < / App1Context . Provider > ) ; } ) ; Apps . forEach ( ( Component ) => { const mount = document . createElement ( "div" ) ; document . body . append ( mount ) ; ReactDOM . createRoot ( mount ) . render ( < Component / > ) ; } ) ;

Working demo here

This is probably the most flexible version. In theory we could also allow the fallback to be a function so if you wanted a fresh version of something you could do that, but ultimately, I don’t want to be responsible for that, and lunch is ready.

After writing a React helper, I feel a strong need to go take a shower. Y’all take care!