-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(utils/react): add injectFCTrampoline
- Loading branch information
1 parent
1b5fd78
commit 44fdf9e
Showing
4 changed files
with
132 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
// 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; | ||
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"); | ||
return Object.create(component.prototype); | ||
}; | ||
} | ||
} | ||
|
||
const removeStubsIfNeeded = () => { | ||
if (stubsApplied) { | ||
loggingEnabled && logger.debug("removed stubs"); | ||
stubsApplied = false; | ||
removeHookStubs(); | ||
window.SP_REACT.createElement = oldCreateElement; | ||
} | ||
} | ||
|
||
// 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); | ||
applyStubsIfNeeded(); | ||
return this._contextType; | ||
}, | ||
set: function (value) { | ||
this._contextType = value; | ||
} | ||
}); | ||
|
||
// Undoes the second contextType access we can't detect shortly before render before it's able to cause any damage | ||
Object.defineProperty(component, "getDerivedStateFromProps", { | ||
configurable: true, | ||
get: function () { | ||
loggingEnabled && logger.debug("get getDerivedStateFromProps", this, stubsApplied); | ||
removeStubsIfNeeded(); | ||
return this._getDerivedStateFromProps; | ||
}, | ||
set: function (value) { | ||
this._getDerivedStateFromProps = value; | ||
} | ||
}); | ||
|
||
// 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); | ||
removeStubsIfNeeded(); | ||
return this._updater = value; | ||
} | ||
}); | ||
|
||
return userComponent; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters