React

Date: 2025-02-14

Hooks

import { DependencyList, useEffect, useState } from "react";
import { debounceTime, fromEvent, map, Observable, Subject } from "rxjs";

export interface IRect {
    width: number;
    height: number;
}

export interface IData<T> {
    data: T | undefined;
    revision: number;
    loading: boolean;
    loaded: boolean;
    initialLoading: boolean;
    setData: (data: T) => unknown,
    updateData: () => unknown
}

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



NPM packages

linq-to-typescript (https://www.npmjs.com/package/linq-to-typescript)
react-icons  (https://react-icons.github.io/react-icons/)
antd (https://ant.design/)


framer-motion
dayjs
d3
date-fns
copy-to-clipboard
rxjs
shortid
nanoid
jwt-decode

Forms

import create from 'zustand';

const useFormStore = create((set) => ({
  formData: {},
  setField: (key: string, value: any) =>
    set((state) => ({ formData: { ...state.formData, [key]: value } })),
}));


const FormField = ({ value, onChange }) => {
  return <input value={value || ''} onChange={(e) => onChange(e.target.value)} />;
};

const App = () => {
  const { formData, setField } = useFormStore();

  return (
    <div>
      <h3>Flexibele Formulier Velden</h3>
      <label>Naam:</label>
      <FormField value={formData.naam} onChange={(val) => setField('naam', val)} />

      <label>Email:</label>
      <FormField value={formData.email} onChange={(val) => setField('email', val)} />

      <button onClick={() => console.log('Submit:', formData)}>Verzenden</button>
    </div>
  );
};

export default App;

import create from 'zustand';

interface IArticleFields {
  title: string;
  description: string;
  price: number;
}


interface FormState {
  formData: Partial<IArticleFields>; // Partial zodat velden optioneel zijn
  setField: <K extends keyof IArticleFields>(key: K, value: IArticleFields[K]) => void;
}

const useFormStore = create<FormState>((set) => ({
  formData: {},
  setField: (key, value) =>
    set((state) => ({
      formData: { ...state.formData, [key]: value },
    })),
}));

const App = () => {
  const { formData, setField } = useFormStore();

  return (
    <div>
      <h3>Artikel Formulier</h3>

      <label>Titel:</label>
      <FormField value={formData.title} onChange={(val) => setField("title", val)} />

      <label>Beschrijving:</label>
      <FormField value={formData.description} onChange={(val) => setField("description", val)} />

      <label>Prijs:</label>
      <FormField
        value={formData.price ? formData.price.toString() : ""}
        onChange={(val) => setField("price", Number(val))}
      />

      <button onClick={() => console.log("Submit:", formData)}>Verzenden</button>
    </div>
  );
};

import { ReactElement } from "react";
import { useFormField } from "./useFormField";

//const [value, setValue] = useState(() => formStore.getValues()[field]);

interface FormFieldProps {
  field: keyof IArticleFields;
  children: ReactElement<{ value: any; onChange: (val: any) => void }>;
}

const FormField = ({ field, children }: FormFieldProps) => {
  const [value, setValue] = useFormField(field);

  return React.cloneElement(children, {
    value,
    onChange: (e: any) => setValue(e.target ? e.target.value : e),
  });
};

export default FormField;






import { BehaviorSubject } from "rxjs";

export class FormStore<T extends Record<string, any>> {
  private subjects: { [K in keyof T]: BehaviorSubject<T[K]> };

  constructor(initialValues: T) {
    this.subjects = Object.keys(initialValues).reduce((acc, key) => {
      acc[key as keyof T] = new BehaviorSubject(initialValues[key as keyof T]);
      return acc;
    }, {} as { [K in keyof T]: BehaviorSubject<T[K]> });
  }

  getField$ = <K extends keyof T>(key: K) => this.subjects[key].asObservable();
  setField = <K extends keyof T>(key: K, value: T[K]) => this.subjects[key].next(value);
  getValues = (): T => Object.keys(this.subjects).reduce((acc, key) => {
    acc[key as keyof T] = this.subjects[key as keyof T].getValue();
    return acc;
  }, {} as T);
}



import { useEffect, useState } from "react";
import { BehaviorSubject } from "rxjs";

export const useBehaviorSubject = <T>(subject: BehaviorSubject<T>) => {
  const [value, setValue] = useState<T>(subject.getValue());

  useEffect(() => {
    const subscription = subject.subscribe(setValue);
    return () => subscription.unsubscribe();
  }, [subject]);

  return [value, (val: T) => subject.next(val)] as const;
};



import { ReactElement } from "react";
import { BehaviorSubject } from "rxjs";
import { useBehaviorSubject } from "./useBehaviorSubject";

interface FormFieldProps<T> {
  field$: BehaviorSubject<T>;
  children: ReactElement<{ value: T; onChange: (val: any) => void }>;
}

const FormField = <T,>({ field$, children }: FormFieldProps<T>) => {
  const [value, setValue] = useBehaviorSubject(field$);

  return React.cloneElement(children, {
    value,
    onChange: (e: any) => setValue(e.target ? e.target.value : e),
  });
};

export default FormField;


const App = () => {
  const formStore = new FormStore<IArticleFields>({
    title: "",
    description: "",
    price: 0,
  });

  return (
    <div>
      <h3>Artikel Formulier</h3>

      <label>Titel:</label>
      <FormField field$={formStore.getField$("title")}>
        <input />
      </FormField>

      <label>Beschrijving:</label>
      <FormField field$={formStore.getField$("description")}>
        <textarea />
      </FormField>

      <label>Prijs:</label>
      <FormField field$={formStore.getField$("price")}>
        <input type="number" />
      </FormField>

      <button onClick={() => console.log("Submit:", formStore.getValues())}>Verzend</button>
    </div>
  );
};

export default App;