Created
January 5, 2020 21:40
-
-
Save malte-wessel/59e4af320fc7ac177275bf4ff0897835 to your computer and use it in GitHub Desktop.
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
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