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;