2024-07-18 00:54:29 -04:00
// Utilities for patching function components
import { createElement , type FC } from 'react' ;
import { applyHookStubs , removeHookStubs } from './react' ;
import Logger from '../../logger' ;
export interface FCTrampoline {
component : FC
}
let loggingEnabled = false ;
export function setFCTrampolineLoggingEnabled ( value : boolean = true ) { loggingEnabled = value } ;
let logger = new Logger ( 'FCTrampoline' ) ;
/ * *
* Directly hooks a function component from its reference , redirecting it to a user - patchable wrapper in its returned object .
* This only works if the original component when called directly returns either nothing , null , or another child element .
*
* This works by tricking react into thinking it ' s a class component by cleverly working around its class component checks ,
* keeping the unmodified function component intact as a mostly working constructor ( as it is impossible to direcly modify a function ) ,
* stubbing out hooks to prevent errors by detecting setter / getter triggers that occur direcly before / after the class component is instantiated by react ,
* and creating a fake class component render method to trampoline out into your own handler .
*
* Due to the nature of this method of hooking a component , please only use this where it is * absolutely necessary . *
* Incorrect hook stubs can cause major instability , be careful when writing them . Refer to fakeRenderComponent for the hook stub implementation .
* Make sure your hook stubs can handle all the cases they could possibly need to within the component you are injecting into .
* You do not need to worry about its children , as they are never called due to the first instance never actually rendering .
* /
export function injectFCTrampoline ( component : FC , customHooks? : any ) : FCTrampoline {
// It needs to be wrapped so React doesn't infinitely call the fake class render method.
const newComponent = function ( this : any , . . . args : any ) {
loggingEnabled && logger . debug ( "new component rendering with props" , args ) ;
return component . apply ( this , args ) ;
}
const userComponent = { component : newComponent } ;
// Create a fake class component render method
component . prototype . render = function ( . . . args : any [ ] ) {
loggingEnabled && logger . debug ( "rendering trampoline" , args , this ) ;
// Pass through rendering via creating the component as a child so React can use function component logic instead of class component logic (setting up the hooks)
return createElement ( userComponent . component , this . props , this . props . children ) ;
} ;
component . prototype . isReactComponent = true ;
let stubsApplied = false ;
2026-03-20 20:27:59 -04:00
const patchJsx = window . SP_REACTDOM . version . startsWith ( "19." ) ;
let oldJsx = window . SP_JSX ? . jsx ;
let oldJsxs = window . SP_JSX ? . jsxs ;
2024-07-18 00:54:29 -04:00
let oldCreateElement = window . SP_REACT . createElement ;
const applyStubsIfNeeded = ( ) = > {
if ( ! stubsApplied ) {
loggingEnabled && logger . debug ( "applied stubs" ) ;
stubsApplied = true ;
applyHookStubs ( customHooks )
// we have to redirect this to return an object with component's prototype as a constructor returning a value overrides its prototype
window . SP_REACT . createElement = ( ) = > {
loggingEnabled && logger . debug ( "createElement hook called" ) ;
2024-08-08 14:18:19 -04:00
loggingEnabled && console . trace ( "createElement trace" ) ;
2024-07-18 00:54:29 -04:00
return Object . create ( component . prototype ) ;
} ;
2026-03-20 20:27:59 -04:00
if ( patchJsx ) {
window . SP_JSX . jsx = ( ) = > {
loggingEnabled && logger . debug ( "jsx hook called" ) ;
loggingEnabled && console . trace ( "jsx trace" ) ;
return Object . create ( component . prototype ) ;
}
window . SP_JSX . jsxs = ( ) = > {
loggingEnabled && logger . debug ( "jsxs hook called" ) ;
loggingEnabled && console . trace ( "jsxs trace" ) ;
return Object . create ( component . prototype ) ;
}
}
2024-07-18 00:54:29 -04:00
}
}
const removeStubsIfNeeded = ( ) = > {
if ( stubsApplied ) {
loggingEnabled && logger . debug ( "removed stubs" ) ;
stubsApplied = false ;
removeHookStubs ( ) ;
window . SP_REACT . createElement = oldCreateElement ;
2026-03-20 20:27:59 -04:00
if ( patchJsx ) {
window . SP_JSX . jsx = oldJsx ;
window . SP_JSX . jsxs = oldJsxs ;
}
2024-07-18 00:54:29 -04:00
}
}
2024-08-08 14:18:19 -04:00
let renderHookStep = 0 ;
2025-10-15 00:04:31 -04:00
if ( window . SP_REACTDOM . version . startsWith ( "19." ) ) {
// Accessed two times directly before class instantiation on path A and once on path B
Object . defineProperty ( component , "contextType" , {
configurable : true ,
get : function ( ) {
loggingEnabled && logger . debug ( "get contexttype" , this , this . _contextType , stubsApplied , renderHookStep ) ;
loggingEnabled && console . trace ( "contextType trace" ) ;
if ( renderHookStep == 0 ) {
renderHookStep = 1 ;
}
if ( this . _contextType == null ) {
this . _contextType = { } ;
}
if ( ! this . _contextType . appliedCurrentValueHook ) {
logger . debug ( "applied currentvalue hook" ) ;
this . _contextType . appliedCurrentValueHook = true ;
Object . defineProperty ( this . _contextType , "_currentValue" , {
configurable : true ,
get : function ( ) {
loggingEnabled && logger . debug ( "get currentValue" , this , stubsApplied , renderHookStep ) ;
loggingEnabled && console . trace ( "currentValue trace" ) ;
if ( renderHookStep == 1 ) {
renderHookStep = 2 ;
applyStubsIfNeeded ( ) ;
}
return this . __currentValue ;
} ,
set : function ( value ) {
return this . __currentValue = value ;
}
} ) ;
}
return this . _contextType ;
} ,
set : function ( value ) {
this . _contextType = value ;
}
} ) ;
2024-07-18 00:54:29 -04:00
2025-10-15 00:04:31 -04:00
// Set directly after class is instantiated
Object . defineProperty ( component . prototype , "updater" , {
configurable : true ,
get : function ( ) {
return this . _updater ;
} ,
set : function ( value ) {
loggingEnabled && logger . debug ( "set updater" , this , value , stubsApplied , renderHookStep ) ;
loggingEnabled && console . trace ( "updater trace" ) ;
if ( renderHookStep == 1 || renderHookStep == 2 ) {
renderHookStep = 0 ;
removeStubsIfNeeded ( ) ;
}
return this . _updater = value ;
}
} ) ;
2024-07-18 00:54:29 -04:00
2025-10-15 00:04:31 -04:00
// Prevents the second contextType access from leaving its hooks around
Object . defineProperty ( component , "getDerivedStateFromProps" , {
configurable : true ,
get : function ( ) {
loggingEnabled && logger . debug ( "get getDerivedStateFromProps" , this , stubsApplied , renderHookStep ) ;
loggingEnabled && console . trace ( "getDerivedStateFromProps trace" ) ;
if ( renderHookStep == 1 || renderHookStep == 2 ) {
renderHookStep = 0 ;
removeStubsIfNeeded ( ) ;
}
return this . _getDerivedStateFromProps ;
} ,
set : function ( value ) {
this . _getDerivedStateFromProps = value ;
2024-08-08 14:18:19 -04:00
}
2025-10-15 00:04:31 -04:00
} ) ;
} else if ( window . SP_REACTDOM . version . startsWith ( "18." ) ) {
// Accessed two times, once directly before class instantiation, and again in some extra logic we don't need to worry about that we hanlde below just in case.
Object . defineProperty ( component , "contextType" , {
configurable : true ,
get : function ( ) {
loggingEnabled && logger . debug ( "get contexttype" , this , stubsApplied , renderHookStep ) ;
loggingEnabled && console . trace ( "contextType trace" ) ;
if ( renderHookStep == 0 ) renderHookStep = 1 ;
else if ( renderHookStep == 3 ) renderHookStep = 4 ;
return this . _contextType ;
} ,
set : function ( value ) {
this . _contextType = value ;
}
} ) ;
2024-07-18 00:54:29 -04:00
2025-10-15 00:04:31 -04:00
// Always accessed directly after contextType for the path we want to catch.
Object . defineProperty ( component , "contextTypes" , {
configurable : true ,
get : function ( ) {
loggingEnabled && logger . debug ( "get contexttypes" , this , stubsApplied , renderHookStep ) ;
loggingEnabled && console . trace ( "contextTypes trace" ) ;
if ( renderHookStep == 1 ) {
renderHookStep = 2 ;
applyStubsIfNeeded ( ) ;
} ;
return this . _contextTypes ;
} ,
set : function ( value ) {
this . _contextTypes = value ;
2024-08-08 14:18:19 -04:00
}
2025-10-15 00:04:31 -04:00
} ) ;
// Set directly after class is instantiated
Object . defineProperty ( component . prototype , "updater" , {
configurable : true ,
get : function ( ) {
return this . _updater ;
} ,
set : function ( value ) {
loggingEnabled && logger . debug ( "set updater" , this , value , stubsApplied , renderHookStep ) ;
loggingEnabled && console . trace ( "updater trace" ) ;
if ( renderHookStep == 2 ) {
renderHookStep = 0 ;
removeStubsIfNeeded ( ) ;
}
return this . _updater = value ;
}
} ) ;
// Prevents the second contextType+contextTypes access from leaving its hooks around
Object . defineProperty ( component , "getDerivedStateFromProps" , {
configurable : true ,
get : function ( ) {
loggingEnabled && logger . debug ( "get getDerivedStateFromProps" , this , stubsApplied , renderHookStep ) ;
loggingEnabled && console . trace ( "getDerivedStateFromProps trace" ) ;
if ( renderHookStep == 2 ) {
renderHookStep = 0 ;
removeStubsIfNeeded ( ) ;
}
return this . _getDerivedStateFromProps ;
} ,
set : function ( value ) {
this . _getDerivedStateFromProps = value ;
}
} ) ;
}
2024-08-08 14:18:19 -04:00
2024-07-18 00:54:29 -04:00
return userComponent ;
}