React

Date: 2025-02-14

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>
    ));
}