1/* eslint-disable react-hooks/exhaustive-deps */
2
3import { useMemo, useEffect, useState } from 'preact/hooks';
4
5import type { Source } from 'wonka';
6import { fromValue, makeSubject, pipe, concat, subscribe } from 'wonka';
7
8type Updater<T> = (input: T) => void;
9
10let currentInit = false;
11
12// Two operations are considered equal if they have the same key
13const areOperationsEqual = (
14 a: { key: number } | undefined,
15 b: { key: number } | undefined
16) => {
17 return a === b || !!(a && b && a.key === b.key);
18};
19
20const isShallowDifferent = (a: any, b: any) => {
21 if (typeof a != 'object' || typeof b != 'object') return a !== b;
22 for (const x in a) if (!(x in b)) return true;
23 for (const key in b) {
24 if (
25 key === 'operation'
26 ? !areOperationsEqual(a[key], b[key])
27 : a[key] !== b[key]
28 ) {
29 return true;
30 }
31 }
32 return false;
33};
34
35export function useSource<T, R>(
36 input: T,
37 transform: (input$: Source<T>, initial?: R) => Source<R>
38): [R, Updater<T>] {
39 const [input$, updateInput] = useMemo((): [Source<T>, (value: T) => void] => {
40 const subject = makeSubject<T>();
41 const source = concat([fromValue(input), subject.source]);
42
43 const updateInput = (nextInput: T) => {
44 if (nextInput !== input) subject.next((input = nextInput));
45 };
46
47 return [source, updateInput];
48 }, []);
49
50 const [state, setState] = useState<R>(() => {
51 currentInit = true;
52 let state: R;
53 try {
54 pipe(
55 transform(fromValue(input)),
56 subscribe(value => {
57 state = value;
58 })
59 ).unsubscribe();
60 } finally {
61 currentInit = false;
62 }
63
64 return state!;
65 });
66
67 useEffect(() => {
68 return pipe(
69 transform(input$, state),
70 subscribe(value => {
71 if (!currentInit) {
72 setState(prevValue => {
73 return isShallowDifferent(prevValue, value) ? value : prevValue;
74 });
75 }
76 })
77 ).unsubscribe;
78 }, [input$ /* `state` is only an initialiser */]);
79
80 return [state, updateInput];
81}