Skip to content

Instantly share code, notes, and snippets.

@baetheus
Last active December 12, 2022 19:13
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save baetheus/d279ec135c2c54ee2c2b0209f1c8f29a to your computer and use it in GitHub Desktop.
Save baetheus/d279ec135c2c54ee2c2b0209f1c8f29a to your computer and use it in GitHub Desktop.
Preact Hooks Experiment for Typescript
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);
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;
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