Last active
December 12, 2022 19:13
-
-
Save baetheus/d279ec135c2c54ee2c2b0209f1c8f29a to your computer and use it in GitHub Desktop.
Preact Hooks Experiment for Typescript
This file contains hidden or 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
import { h, FunctionalComponent, render, options } from 'preact'; | |
import { handleVnode } from './hooks'; | |
// Wireup experimental hooks | |
options.vnode = handleVnode; | |
import Test from './component'; | |
export const Main: FunctionalComponent<any> = () => ( | |
<main className="fld-column"> | |
<header> | |
<h1> | |
Bundle Testing <small className="test">Subheader</small> | |
</h1> | |
</header> | |
<article> | |
<p>Some Stuff</p> | |
<Test text="Some Text" /> | |
</article> | |
<footer> | |
<h6>Footer</h6> | |
</footer> | |
</main> | |
); | |
render(<Main />, document.body); |
This file contains hidden or 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
import { h, FunctionalComponent } from 'preact'; | |
import { useState } from './hooks'; | |
export interface TestProps { | |
text: string; | |
} | |
/** | |
* @render react | |
* @name Test | |
* @example | |
* <Test text="Hello World" /> | |
*/ | |
export const Test: FunctionalComponent<TestProps> = ({ text }) => { | |
const [state, setState] = useState({ count: 0 }); | |
const step = (i: number) => () => setState(s => ({ ...s, count: s.count + i })); | |
const inc = step(1); | |
const dec = step(-1); | |
return ( | |
<section className="ba-7 ba-sm-2 ba-solid"> | |
<h1>{text}</h1> | |
<p>Count: {state.count}</p> | |
<p>Click to increase count.</p> | |
<button onClick={inc}>Increase</button> | |
<button onClick={dec}>Decrease</button> | |
</section> | |
); | |
}; | |
export default Test; |
This file contains hidden or 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
import { Component, options } from 'preact'; | |
/** | |
* Experimental Hooks API | |
* | |
* Pulled from https://codesandbox.io/s/mnox05qp8 | |
* Added very generic types | |
* | |
* To use, import handleVnode and assign it to options.vnode | |
* | |
* import { options } from 'preact'; | |
* import { handleVnode } from './hooks'; | |
* | |
* options.vnode = handleVnode; | |
*/ | |
// This interface could be tightened up | |
interface HookContext { | |
hookIndex: number; | |
hooks: any[]; | |
hookDeps: Array<any[]>; | |
hooksCleanups: Array<() => void | undefined>; | |
layoutEffects: Array<() => void | undefined>; | |
render?: Component['render']; | |
props?: Component['props']; | |
} | |
// HOOKS | |
let hookContext: HookContext = { | |
hookIndex: 0, | |
hooks: [], | |
hookDeps: [], | |
hooksCleanups: [], | |
layoutEffects: [], | |
}; | |
// public API | |
export function useEffect(effect: () => () => void, deps: any[] = []) { | |
const i = hookContext.hookIndex++; | |
if (!hookContext.hooks[i]) { | |
hookContext.hooks[i] = effect; | |
hookContext.hookDeps[i] = deps; | |
hookContext.hooksCleanups[i] = effect(); | |
} else { | |
if (deps && !sameArray(deps, hookContext.hookDeps[i])) { | |
if (hookContext.hooksCleanups[i]) { | |
hookContext.hooksCleanups[i](); | |
hookContext.hookDeps[i] = deps; | |
} | |
hookContext.hooksCleanups[i] = effect(); | |
} | |
} | |
} | |
export function useState<S>(initial: S) { | |
const i = hookContext.hookIndex++; | |
if (!hookContext.hooks[i]) { | |
hookContext.hooks[i] = { | |
state: initial, | |
}; | |
} | |
const thisHookContext = hookContext; | |
return [ | |
hookContext.hooks[i].state, | |
useCallback((newState: S | ((s: S) => S)) => { | |
thisHookContext.hooks[i].state = transformState(newState, thisHookContext.hooks[i].state); | |
if (thisHookContext.render !== undefined) { | |
thisHookContext.render(thisHookContext.props); | |
} | |
}), | |
] as [S, (newState: S | ((s: S) => S)) => void]; | |
} | |
export function useCallback<T>(cb: (...as: any[]) => T, deps: any[] = []) { | |
return useMemo(() => cb, deps); | |
} | |
export function useMemo<S>(factory: () => () => S, deps: any[] = []) { | |
const i = hookContext.hookIndex++; | |
if (!hookContext.hooks[i] || !deps || !sameArray(deps, hookContext.hookDeps[i])) { | |
hookContext.hooks[i] = factory(); | |
hookContext.hookDeps[i] = deps; | |
} | |
return hookContext.hooks[i]; | |
} | |
export function useReducer<S>(reducer: (s: S, a: unknown) => S, initialState: S, initialAction: unknown) { | |
const i = hookContext.hookIndex++; | |
if (!hookContext.hooks[i]) { | |
hookContext.hooks[i] = { | |
state: initialAction ? reducer(initialState, initialAction) : initialState, | |
}; | |
} | |
const thisHookContext = hookContext; | |
return [ | |
hookContext.hooks[i].state, | |
useCallback(action => { | |
thisHookContext.hooks[i].state = reducer(thisHookContext.hooks[i].state, action); | |
if (thisHookContext.render !== undefined) { | |
thisHookContext.render(thisHookContext.props); | |
} | |
}, []), | |
] as [S, ((a: unknown) => void)]; | |
} | |
export function useRef<S>(initialValue: S) { | |
return useCallback(refHolderFactory(initialValue), []); | |
} | |
export function useLayoutEffect(effect: () => () => void, deps: any[] = []) { | |
const i = hookContext.hookIndex++; | |
const thisHookContext = hookContext; | |
useEffect(() => { | |
thisHookContext.layoutEffects[i] = () => { | |
thisHookContext.hooksCleanups[i] = effect(); | |
}; | |
return () => undefined; | |
}, deps); | |
} | |
// end public api | |
function transformState<S>(state: S | ((s: S) => S), prevState: S) { | |
if (typeof state === 'function') { | |
return (<(s: S) => S>state)(prevState); | |
} | |
return state; | |
} | |
function sameArray(arr1: any[], arr2: any[]) { | |
if (arr1.length !== arr2.length) { | |
return false; | |
} | |
for (let i = 0; i < arr1.length; ++i) { | |
if (arr1[i] !== arr2[i]) { | |
return false; | |
} | |
} | |
return true; | |
} | |
function refHolderFactory<S>(reference: S) { | |
function RefHolder(ref: S) { | |
reference = ref; | |
} | |
Object.defineProperty(RefHolder, 'current', { | |
get: () => reference, | |
enumerable: true, | |
configurable: true, | |
}); | |
return RefHolder; | |
} | |
// Wrapper Class | |
class HooksWrapper extends Component { | |
hookIndex: number = 0; | |
hooks: HookContext['hooks'] = []; | |
hooksCleanups: HookContext['hooksCleanups'] = []; | |
hookDeps: HookContext['hookDeps'] = []; | |
layoutEffects: HookContext['layoutEffects'] = []; | |
originalRender: any; | |
constructor(originalRender: any) { | |
super(); | |
this.originalRender = originalRender; | |
} | |
componentDidMount() { | |
for (let i = 0; i < this.hooks.length; ++i) { | |
const effect = this.layoutEffects[i]; | |
if (effect) { | |
try { | |
effect(); | |
} catch (e) {} | |
} | |
} | |
this.layoutEffects = []; | |
} | |
componentDidUpdate() { | |
for (let i = 0; i < this.hooks.length; ++i) { | |
const effect = this.layoutEffects[i]; | |
if (effect) { | |
try { | |
effect(); | |
} catch (e) {} | |
} | |
} | |
this.layoutEffects = []; | |
} | |
componentWillUnmount() { | |
for (let i = 0; i < this.hooks.length; ++i) { | |
const cleanup = this.hooksCleanups[i]; | |
if (cleanup) { | |
try { | |
cleanup(); | |
} catch (e) {} | |
} | |
} | |
} | |
render(...args: any[]) { | |
const prevContext = hookContext; | |
try { | |
hookContext = this; | |
this.hookIndex = 0; | |
return this.originalRender(...args); | |
} finally { | |
hookContext = prevContext; | |
} | |
} | |
} | |
export const handleVnode: typeof options.vnode = function(node) { | |
if (typeof node.nodeName === 'function' && !(node.nodeName.prototype && node.nodeName.prototype.render)) { | |
// only modern bind as described in MDN, will not work in IE | |
node.nodeName = HooksWrapper.bind(null, node.nodeName); | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment