Optionally Shared Context in React Components
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!