Hooks
export function useEffectAsync(func: () => unknown, deps?: DependencyList) {
useEffect(() => {
func();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
}
export interface IUseDataState<T> {
data: T | undefined;
trigger: number;
revision: number;
loading: boolean;
loaded: boolean;
initialLoading: boolean;
}
export function useData<T>(fetchFn: () => Promise<T>, deps: any = []): IData<T> {
const [state, setState] = useObjectState<IUseDataState<T>>({
revision: 0,
trigger: 0,
loaded: false,
loading: true,
initialLoading: true,
data: undefined
});
useEffectAsync(async () => {
if (!state.initialLoading) {
setState({ loading: true, loaded: false });
}
let newState: Partial<IUseDataState<T>> = {};
try {
const newData = await fetchFn();
newState = { data: newData, loaded: true, revision: state.revision + 1 };
} catch (e) {
console.error(e);
newState = { data: undefined, revision: state.revision + 1 };
} finally {
setState({ ...newState, loading: false, initialLoading: false });
}
}, [...deps, state.trigger]);
return {
data: state.data,
revision: state.revision,
loading: state.loading,
loaded: state.loaded,
initialLoading: state.initialLoading,
setData: (data: T) => setState({ data: data, revision: state.revision + 1 }),
updateData: () => setState({ trigger: state.trigger + 1 })
} as IData<T>;
}
export function useIsMobile(): boolean {
const bp = 768;
const [isMobile, setIsMobile] = useState(window.innerWidth < bp);
useEffect(() => {
const updateSize = (windowSize: number): void => {
setIsMobile(windowSize < bp);
};
const resizeEvent = fromEvent(window, "resize");
resizeEvent.pipe(
map(x => (x.target as Window).innerWidth),
debounceTime(500)
).subscribe(updateSize);
}, []);
return isMobile;
}
export function useObservable<T>(subject: Observable<T | undefined>, initialValue?: T): T | undefined {
const [data, setData] = useState<T | undefined>(initialValue);
useEffect(() => {
if (!subject) { return; }
const subscription = subject.subscribe((x: any) => {
setData(x);
});
return () => subscription.unsubscribe();
}, [subject]);
return data;
}
export function useElementSize(ref: React.RefObject<HTMLElement>) {
const [size, setSize] = useState<IRect>({ height: 0, width: 0 });
useEffect(() => {
const element = ref.current;
if (!element) return;
const onSizeChanged = () => {
if (!element) return;
const rect = element.getBoundingClientRect();
setSize(rect);
};
const observer = new (window as any).ResizeObserver(onSizeChanged);
observer.observe(element);
return () => observer.unobserve(element);
}, [ref]);
return size;
}
export function useObjectState<T>(initialValue: T): [T, (c: Partial<T>) => any] {
const [item, setItem] = useState<T>(initialValue);
const handleChange = (changes: Partial<T>) => setItem(existing => ({ ...existing, ...changes }));
return [item, handleChange];
}
export function useStateDebounce<T>(time: number, initialValue: T): [T, (v: T) => any] {
const [value, setValue] = useState<T>(initialValue)
const [values] = useState(() => new Subject<T>())
useEffect(() => {
const sub = values.pipe(debounceTime(time)).subscribe(setValue)
return () => sub.unsubscribe()
}, [time, values])
return [value, (v: T) => values.next(v)];
}
Components
export function TextWithNewLines(props: {text: string}) {
const {text} = props;
const lines = text.replace(/\r\n(?![.!?]|\s*[A-Z])/g, " ");
return lines.split(/\r\n/).map((line, index) => (
<React.Fragment key={index}>
{line.trim()}
<br />
</React.Fragment>
));
}