Skip to content

Instantly share code, notes, and snippets.

@malte-wessel
Created January 5, 2020 21:40
Show Gist options
  • Save malte-wessel/59e4af320fc7ac177275bf4ff0897835 to your computer and use it in GitHub Desktop.
Save malte-wessel/59e4af320fc7ac177275bf4ff0897835 to your computer and use it in GitHub Desktop.
import {
useRef,
useEffect,
FC,
ReactElement,
useState,
MutableRefObject,
} from 'react';
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { shallowEqual } from './shallowEqual';
export function useRefFn<T extends object | number | string | boolean | symbol>(
init: () => T,
) {
const ref = useRef<T | null>(null);
if (ref.current === null) {
ref.current = init();
}
return ref as MutableRefObject<T>;
}
export interface ComponentFunctionApi<P> {
props: BehaviorSubject<P>;
updates: Subject<P>;
subscribe: (obs: Observable<any>) => void;
}
export type ComponentFunction<P, S> = (
api: ComponentFunctionApi<P>,
) => Observable<S>;
export type ComponentTemplate<S> = (state: S) => ReactElement | null;
interface ComponentInternals<P extends object> {
propsStream: BehaviorSubject<P>;
updatesStream: Subject<P>;
subscriptions: Subscription[];
}
const createInternals = <P extends object>(p: P) => {
const props = new BehaviorSubject<P>(p);
const updates = new Subject<P>();
const subscriptions: Subscription[] = [];
const subscribe = (obs: Observable<unknown>) =>
subscriptions.push(obs.subscribe());
const dispose = () => {
props.complete();
updates.complete();
subscriptions.forEach(sub => sub.unsubscribe());
subscriptions.length = 0;
};
return {
props,
updates,
subscribe,
subscriptions,
dispose,
};
};
const useRerender = () => {
const [, setState] = useState(0);
return () => setState(state => state + 1);
};
export const createComponent = <P extends object, S>(
componentFunction: ComponentFunction<P, S>,
template: ComponentTemplate<S>,
): FC<P> => (props: P) => {
// Tracks that the function is currenlty being called
const isRunning = useRef(false);
isRunning.current = true;
// Create internals once
const internals = useRefFn(() => createInternals(props));
// Stores the state emitted from component function
const stateRef = useRef<S>();
// Helper for rerendering when component function is not running
const rerender = useRerender();
// Initialize component function stream
if (!internals.current.subscriptions.length) {
const subscription = componentFunction({
props: internals.current.props,
updates: internals.current.updates,
subscribe: internals.current.subscribe,
})
.pipe(distinctUntilChanged(shallowEqual))
.subscribe(state => {
stateRef.current = state;
if (!isRunning.current) {
rerender();
}
});
internals.current.subscriptions.push(subscription);
}
// Push received props to props stream
internals.current.props.next(props);
// Push updates stream
useEffect(() => internals.current.updates.next(props));
// Dispose on unmount
useEffect(() => internals.current.dispose, []);
// On server side dispose observables immediately
if (!process.browser) {
internals.current.dispose();
}
// Let component now that we are done
isRunning.current = false;
// Render current state
if (stateRef.current !== undefined) {
return template(stateRef.current);
}
return null;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment