{"id":9265,"date":"2025-02-14T15:07:50","date_gmt":"2025-02-14T14:07:50","guid":{"rendered":"https:\/\/solidt.eu\/site\/?page_id=9265"},"modified":"2025-11-07T14:15:45","modified_gmt":"2025-11-07T13:15:45","slug":"react","status":"publish","type":"page","link":"https:\/\/solidt.eu\/site\/react\/","title":{"rendered":"React"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">Hooks<\/h2>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"tsx\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">import { DependencyList, useEffect, useState } from \"react\";\nimport { debounceTime, fromEvent, map, Observable, Subject } from \"rxjs\";\n\nexport interface IRect {\n    width: number;\n    height: number;\n}\n\nexport interface IData&lt;T> {\n    data: T | undefined;\n    revision: number;\n    loading: boolean;\n    loaded: boolean;\n    initialLoading: boolean;\n    setData: (data: T) => unknown,\n    updateData: () => unknown\n}\n\nexport function useEffectAsync(func: () => unknown, deps?: DependencyList) {\n    useEffect(() => {\n        func();\n        \/\/ eslint-disable-next-line react-hooks\/exhaustive-deps\n    }, deps);\n}\n\nexport interface IUseDataState&lt;T> {\n    data: T | undefined;\n    trigger: number;\n    revision: number;\n    loading: boolean;\n    loaded: boolean;\n    initialLoading: boolean;\n}\n\nexport function useData&lt;T>(fetchFn: () => Promise&lt;T>, deps: any = []): IData&lt;T> {\n\n    const [state, setState] = useObjectState&lt;IUseDataState&lt;T>>({\n        revision: 0,\n        trigger: 0,\n        loaded: false,\n        loading: true,\n        initialLoading: true,\n        data: undefined\n    });\n\n    useEffectAsync(async () => {\n        if (!state.initialLoading) {\n            setState({ loading: true, loaded: false });\n        }\n        let newState: Partial&lt;IUseDataState&lt;T>> = {};\n        try {\n            const newData = await fetchFn();\n            newState = { data: newData, loaded: true, revision: state.revision + 1 };\n        } catch (e) {\n            console.error(e);\n            newState = { data: undefined, revision: state.revision + 1 };\n        } finally {\n            setState({ ...newState, loading: false, initialLoading: false });\n        }\n    }, [...deps, state.trigger]);\n\n    return {\n        data: state.data,\n        revision: state.revision,\n        loading: state.loading,\n        loaded: state.loaded,\n        initialLoading: state.initialLoading,\n        setData: (data: T) => setState({ data: data, revision: state.revision + 1 }),\n        updateData: () => setState({ trigger: state.trigger + 1 })\n    } as IData&lt;T>;\n}\n\n\nexport function useIsMobile(): boolean {\n    const bp = 768;\n    const [isMobile, setIsMobile] = useState(window.innerWidth &lt; bp);\n    useEffect(() => {\n        const updateSize = (windowSize: number): void => {\n            setIsMobile(windowSize &lt; bp);\n        };\n\n        const resizeEvent = fromEvent(window, \"resize\");\n        resizeEvent.pipe(\n            map(x => (x.target as Window).innerWidth),\n            debounceTime(500)\n        ).subscribe(updateSize);\n    }, []);\n    return isMobile;\n}\n\nexport function useObservable&lt;T>(subject: Observable&lt;T | undefined>, initialValue?: T): T | undefined {\n    const [data, setData] = useState&lt;T | undefined>(initialValue);\n    useEffect(() => {\n        if (!subject) { return; }\n        const subscription = subject.subscribe((x: any) => {\n            setData(x);\n        });\n        return () => subscription.unsubscribe();\n    }, [subject]);\n    return data;\n}\n\nexport function useElementSize(ref: React.RefObject&lt;HTMLElement>) {\n    const [size, setSize] = useState&lt;IRect>({ height: 0, width: 0 });\n    useEffect(() => {\n        const element = ref.current;\n        if (!element) return;\n        const onSizeChanged = () => {\n            if (!element) return;\n            const rect = element.getBoundingClientRect();\n            setSize(rect);\n        };\n        const observer = new (window as any).ResizeObserver(onSizeChanged);\n        observer.observe(element);\n        return () => observer.unobserve(element);\n    }, [ref]);\n    return size;\n}\n\n\nexport function useObjectState&lt;T>(initialValue: T): [T, (c: Partial&lt;T>) => any] {\n    const [item, setItem] = useState&lt;T>(initialValue);\n    const handleChange = (changes: Partial&lt;T>) => setItem(existing => ({ ...existing, ...changes }));\n    return [item, handleChange];\n}\n\nexport function useStateDebounce&lt;T>(time: number, initialValue: T): [T, (v: T) => any] {\n    const [value, setValue] = useState&lt;T>(initialValue)\n    const [values] = useState(() => new Subject&lt;T>())\n    useEffect(() => {\n        const sub = values.pipe(debounceTime(time)).subscribe(setValue)\n        return () => sub.unsubscribe()\n    }, [time, values])\n    return [value, (v: T) => values.next(v)];\n}\n\n\n<\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Components<\/h2>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"tsx\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">import React from \"react\";\n\nexport function isNullOrEmpty(text: string | undefined) {\n    if (text === undefined || text === null || String(text).trim() === \"\")\n        return true;\n    return false;\n}\n\nexport function TextWithLines(props: { text: string | undefined }) {\n    const { text } = props;\n    const lines = String(text ?? \"\").replace(\/\\r\\n(?![.!?]|\\s*[A-Z])\/g, \" \");\n    return lines.split(\/\\r\\n\/).map((line, index) => (\n        &lt;React.Fragment key={index}>\n            {line.trim()}\n            &lt;br \/>\n        &lt;\/React.Fragment>\n    ));\n}\n\ntype AutoTextWithLinesProps = {\n    text: string | undefined\n} &amp; React.HTMLAttributes&lt;HTMLDivElement>; \/\/ \ud83d\udc48 alle standaard div-props\n\nexport function AutoTextWithLines({ text, ...restOfProps }: AutoTextWithLinesProps) {\n    if (isNullOrEmpty(text)) return &lt;>&lt;\/>;\n    return &lt;div {...restOfProps}>&lt;TextWithLines text={text} \/>&lt;\/div>;\n}\n\/\/ -----\n\nimport { Property } from \"csstype\";\nimport React, { ReactNode } from \"react\";\n\nexport function Align(props: { h?: Property.JustifyItems; v?: Property.AlignItems; children: ReactNode; }) {\n    let style: React.CSSProperties = {};\n\n    if (props.h || props.v)\n        style = { ...style, display: \"grid\" };\n\n    if (props.h) {\n        style = { ...style, width: \"100%\", justifyItems: props.h };\n    }\n\n    if (props.v) {\n        style = { ...style, height: \"100%\", alignItems: props.v };\n    }\n    return &lt;div style={style}>\n        {props.children}\n    &lt;\/div>;\n}\n\n\n\/**\n * Custom hook om een queryparameter in de hash (#) van de URL te lezen en te schrijven.\n * Werkt met hash-routing zoals:  \/#\/stock\/by-location?location=1234\n * werkt url bij, maar triggert geen refresh\n *\/\nexport function useHashSearchParam(paramName: string) {\n    const getCurrentValue = useCallback(() => {\n        const [, queryPart] = window.location.hash.split(\"?\");\n        return new URLSearchParams(queryPart ?? \"\").get(paramName) ?? \"\";\n    }, [paramName]);\n\n    const [value, setValue] = useState&lt;string>(getCurrentValue);\n\n    \/\/ Update lokale state als gebruiker via back\/forward navigeert\n    useEffect(() => {\n        const handler = () => setValue(getCurrentValue());\n        window.addEventListener(\"hashchange\", handler);\n        return () => window.removeEventListener(\"hashchange\", handler);\n    }, [getCurrentValue]);\n\n    \/\/ Update hash zonder herlaad\/navigatie\n    const setHashParam = useCallback(\n        (newValue?: string) => {\n            const hash = window.location.hash || \"\";\n            const [pathPart, queryPart] = hash.split(\"?\");\n            const params = new URLSearchParams(queryPart ?? \"\");\n\n            if (newValue) params.set(paramName, newValue);\n            else params.delete(paramName);\n\n            const newHash =\n                params.toString().length > 0\n                    ? `${pathPart}?${params.toString()}`\n                    : pathPart;\n\n            window.history.replaceState({}, \"\", `${window.location.pathname}${newHash}`);\n            setValue(newValue ?? \"\");\n        },\n        [paramName]\n    );\n\n    return [value, setHashParam] as const;\n}\n<\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">NPM packages<\/h2>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"plain_text\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">linq-to-typescript (https:\/\/www.npmjs.com\/package\/linq-to-typescript)\nreact-icons  (https:\/\/react-icons.github.io\/react-icons\/)\nantd (https:\/\/ant.design\/)\n\n\nframer-motion\ndayjs\nd3\ndate-fns\ncopy-to-clipboard\nrxjs\nshortid\nnanoid\njwt-decode\n\n<\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Forms<\/h2>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"tsx\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">import create from 'zustand';\n\nconst useFormStore = create((set) => ({\n  formData: {},\n  setField: (key: string, value: any) =>\n    set((state) => ({ formData: { ...state.formData, [key]: value } })),\n}));\n\n\nconst FormField = ({ value, onChange }) => {\n  return &lt;input value={value || ''} onChange={(e) => onChange(e.target.value)} \/>;\n};\n\nconst App = () => {\n  const { formData, setField } = useFormStore();\n\n  return (\n    &lt;div>\n      &lt;h3>Flexibele Formulier Velden&lt;\/h3>\n      &lt;label>Naam:&lt;\/label>\n      &lt;FormField value={formData.naam} onChange={(val) => setField('naam', val)} \/>\n\n      &lt;label>Email:&lt;\/label>\n      &lt;FormField value={formData.email} onChange={(val) => setField('email', val)} \/>\n\n      &lt;button onClick={() => console.log('Submit:', formData)}>Verzenden&lt;\/button>\n    &lt;\/div>\n  );\n};\n\nexport default App;\n\n<\/pre><\/div>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"tsx\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">import create from 'zustand';\n\ninterface IArticleFields {\n  title: string;\n  description: string;\n  price: number;\n}\n\n\ninterface FormState {\n  formData: Partial&lt;IArticleFields>; \/\/ Partial zodat velden optioneel zijn\n  setField: &lt;K extends keyof IArticleFields>(key: K, value: IArticleFields[K]) => void;\n}\n\nconst useFormStore = create&lt;FormState>((set) => ({\n  formData: {},\n  setField: (key, value) =>\n    set((state) => ({\n      formData: { ...state.formData, [key]: value },\n    })),\n}));\n\nconst App = () => {\n  const { formData, setField } = useFormStore();\n\n  return (\n    &lt;div>\n      &lt;h3>Artikel Formulier&lt;\/h3>\n\n      &lt;label>Titel:&lt;\/label>\n      &lt;FormField value={formData.title} onChange={(val) => setField(\"title\", val)} \/>\n\n      &lt;label>Beschrijving:&lt;\/label>\n      &lt;FormField value={formData.description} onChange={(val) => setField(\"description\", val)} \/>\n\n      &lt;label>Prijs:&lt;\/label>\n      &lt;FormField\n        value={formData.price ? formData.price.toString() : \"\"}\n        onChange={(val) => setField(\"price\", Number(val))}\n      \/>\n\n      &lt;button onClick={() => console.log(\"Submit:\", formData)}>Verzenden&lt;\/button>\n    &lt;\/div>\n  );\n};\n\n<\/pre><\/div>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"tsx\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">import { ReactElement } from \"react\";\nimport { useFormField } from \".\/useFormField\";\n\n\/\/const [value, setValue] = useState(() => formStore.getValues()[field]);\n\ninterface FormFieldProps {\n  field: keyof IArticleFields;\n  children: ReactElement&lt;{ value: any; onChange: (val: any) => void }>;\n}\n\nconst FormField = ({ field, children }: FormFieldProps) => {\n  const [value, setValue] = useFormField(field);\n\n  return React.cloneElement(children, {\n    value,\n    onChange: (e: any) => setValue(e.target ? e.target.value : e),\n  });\n};\n\nexport default FormField;\n\n\n\n\n\n\n<\/pre><\/div>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"tsx\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">import { BehaviorSubject } from \"rxjs\";\n\nexport class FormStore&lt;T extends Record&lt;string, any>> {\n  private subjects: { [K in keyof T]: BehaviorSubject&lt;T[K]> };\n\n  constructor(initialValues: T) {\n    this.subjects = Object.keys(initialValues).reduce((acc, key) => {\n      acc[key as keyof T] = new BehaviorSubject(initialValues[key as keyof T]);\n      return acc;\n    }, {} as { [K in keyof T]: BehaviorSubject&lt;T[K]> });\n  }\n\n  getField$ = &lt;K extends keyof T>(key: K) => this.subjects[key].asObservable();\n  setField = &lt;K extends keyof T>(key: K, value: T[K]) => this.subjects[key].next(value);\n  getValues = (): T => Object.keys(this.subjects).reduce((acc, key) => {\n    acc[key as keyof T] = this.subjects[key as keyof T].getValue();\n    return acc;\n  }, {} as T);\n}\n\n\n\nimport { useEffect, useState } from \"react\";\nimport { BehaviorSubject } from \"rxjs\";\n\nexport const useBehaviorSubject = &lt;T>(subject: BehaviorSubject&lt;T>) => {\n  const [value, setValue] = useState&lt;T>(subject.getValue());\n\n  useEffect(() => {\n    const subscription = subject.subscribe(setValue);\n    return () => subscription.unsubscribe();\n  }, [subject]);\n\n  return [value, (val: T) => subject.next(val)] as const;\n};\n\n\n\nimport { ReactElement } from \"react\";\nimport { BehaviorSubject } from \"rxjs\";\nimport { useBehaviorSubject } from \".\/useBehaviorSubject\";\n\ninterface FormFieldProps&lt;T> {\n  field$: BehaviorSubject&lt;T>;\n  children: ReactElement&lt;{ value: T; onChange: (val: any) => void }>;\n}\n\nconst FormField = &lt;T,>({ field$, children }: FormFieldProps&lt;T>) => {\n  const [value, setValue] = useBehaviorSubject(field$);\n\n  return React.cloneElement(children, {\n    value,\n    onChange: (e: any) => setValue(e.target ? e.target.value : e),\n  });\n};\n\nexport default FormField;\n\n\nconst App = () => {\n  const formStore = new FormStore&lt;IArticleFields>({\n    title: \"\",\n    description: \"\",\n    price: 0,\n  });\n\n  return (\n    &lt;div>\n      &lt;h3>Artikel Formulier&lt;\/h3>\n\n      &lt;label>Titel:&lt;\/label>\n      &lt;FormField field$={formStore.getField$(\"title\")}>\n        &lt;input \/>\n      &lt;\/FormField>\n\n      &lt;label>Beschrijving:&lt;\/label>\n      &lt;FormField field$={formStore.getField$(\"description\")}>\n        &lt;textarea \/>\n      &lt;\/FormField>\n\n      &lt;label>Prijs:&lt;\/label>\n      &lt;FormField field$={formStore.getField$(\"price\")}>\n        &lt;input type=\"number\" \/>\n      &lt;\/FormField>\n\n      &lt;button onClick={() => console.log(\"Submit:\", formStore.getValues())}>Verzend&lt;\/button>\n    &lt;\/div>\n  );\n};\n\nexport default App;\n\n\n<\/pre><\/div>\n\n\n\n<p>More helper code<\/p>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"tsx\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">import { App } from \"antd\";\nimport { IColumnMetaData } from \"domain\/base\/IListView\";\nimport { EventNames } from \"domain\/events\/EventNames\";\nimport DOMPurify from \"dompurify\";\nimport { DependencyList, Dispatch, ReactNode, RefObject, SetStateAction, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useParams, useSearchParams } from \"react-router-dom\";\nimport { BehaviorSubject, debounceTime, fromEvent, map, Observable, Subject } from \"rxjs\";\nimport { appEmit, appSubscribe } from \"..\/domain\/DomainPorts\";\nimport { ISubscribable } from \".\/Mutables\/ISubscribable\";\nimport { makeMut } from \".\/Mutables\/Mutable\";\nimport { IRect } from \".\/ParentSize\";\nimport { getDate } from \".\/tshelper\";\nimport { IEventSubscription } from \"domain\/events\/IEventHandler\";\n\nexport const When = (value: boolean, e: React.ReactNode) => value ? e : &lt;>&lt;\/>;\nexport const OnlyWhen = (value: boolean, fn: () => React.JSX.Element) => value ? fn() : &lt;>&lt;\/>;\nexport const OnlyWhenFalse = (value: boolean, fn: () => React.JSX.Element) => !value ? fn() : &lt;>&lt;\/>;\nexport const WhenElse = (value: boolean, e: React.ReactNode, n: React.ReactNode) => value ? e : n;\n\nexport interface IDataSource&lt;T> {\n    data: T[];\n    loading: boolean;\n    setData: Dispatch&lt;SetStateAction&lt;T[]>>;\n    updateData: () => any;\n}\n\nexport interface IData&lt;T> {\n    data: T;\n    loading: boolean;\n    revision: number;\n    loaded: boolean;\n    setData: Dispatch&lt;SetStateAction&lt;T>>;\n    updateData: () => any;\n}\n\nexport interface ILazyLoadingData&lt;T> {\n    data?: T;\n    loading: boolean;\n    loaded: boolean;\n    isChanged: boolean;\n    load: () => any;\n}\nexport function RenderHtml(htmlString: string): ReactNode {\n    return &lt;div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(htmlString) }} \/>;\n}\n\nexport function mapData&lt;T>(columns: IColumnMetaData[], data: any[]) {\n    const obj = {} as Record&lt;string, any>;\n    columns.forEach(({ name: fieldName }, i) => {\n        obj[fieldName] = data[i];\n    });\n    obj.key = data[0];\n    return obj as T;\n}\n\nexport function useDatasource&lt;T>(fetchFn: () => Promise&lt;T[]>, deps: any[] = []): IDataSource&lt;T> {\n    const [trigger, setTrigger] = useState&lt;boolean>(false);\n    const [data, setData] = useState&lt;T[]>([]);\n    const [loading, setLoading] = useState(true);\n    const [loaded, setLoaded] = useState(false);\n    useEffect(() => {\n        async function fetch() {\n            setLoaded(false);\n            setLoading(true);\n            try {\n                const newData = await fetchFn();\n                setData(newData);\n                setLoaded(true);\n            } finally {\n                setLoading(false);\n            }\n        }\n        fetch();\n        \/\/ eslint-disable-next-line react-hooks\/exhaustive-deps\n    }, [...deps, trigger]);\n    return {\n        data,\n        loading,\n        loaded,\n        setData,\n        updateData: () => setTrigger(!trigger)\n    } as IDataSource&lt;T>;\n}\n\nexport function useEffectAsync(func: () => unknown, deps?: DependencyList) {\n    useEffect(() => {\n        func();\n        \/\/ eslint-disable-next-line react-hooks\/exhaustive-deps\n    }, deps);\n}\n\n\/\/ New useData version:\nexport interface IUseDataState&lt;T> {\n    data: T | undefined;\n    trigger: number;\n    revision: number;\n    loading: boolean;\n    loaded: boolean;\n    initialLoading: boolean;\n}\n\nexport function useData&lt;T>(fetchFn: (currentData: IUseDataState&lt;T> | undefined) => Promise&lt;T>, deps: any = []): IData&lt;T> {\n\n    const [state, setState] = useObjectState&lt;IUseDataState&lt;T>>({\n        revision: 0,\n        trigger: 0,\n        loaded: false,\n        loading: true,\n        initialLoading: true,\n        data: undefined\n    });\n\n    useEffectAsync(async () => {\n        if (!state.initialLoading) {\n            setState({ loading: true, loaded: false });\n        }\n        let newState: Partial&lt;IUseDataState&lt;T>> = {};\n        try {\n            const newData = await fetchFn(state);\n            newState = { data: newData, loaded: true, revision: state.revision + 1 };\n        } catch (e) {\n            console.error(e);\n            newState = { data: undefined, revision: state.revision + 1 };\n        } finally {\n            setState({ ...newState, loading: false, initialLoading: false });\n        }\n    }, [...deps, state.trigger]);\n    return {\n        data: state.data,\n        revision: state.revision,\n        loading: state.loading,\n        loaded: state.loaded,\n        initialLoading: state.initialLoading,\n        setData: (data: T) => setState({ data: data, revision: state.revision + 1 }),\n        updateData: () => setState({ trigger: state.trigger + 1 })\n    } as IData&lt;T>;\n}\n\nexport function useLazyLoadData&lt;T>(fetchFn: () => Promise&lt;T>, isDefaultChanged = false, deps: any = []): ILazyLoadingData&lt;T> {\n    const [data, setData] = useState&lt;T>();\n    const [loading, setLoading] = useState(false);\n    const [loaded, setLoaded] = useState(false);\n    const [isChanged, setIsChanged] = useState(isDefaultChanged);\n\n    async function loadData() {\n        setLoading(true);\n        setLoaded(false);\n        try {\n            const newData = await fetchFn();\n            setData(newData);\n            setLoaded(true);\n            setIsChanged(false);\n        } catch (e) {\n            console.error(e);\n            setData(undefined);\n        } finally {\n            setLoading(false);\n        }\n    }\n\n    useEffect(() => {\n        setIsChanged(true);\n        \/\/ eslint-disable-next-line react-hooks\/exhaustive-deps\n    }, deps);\n\n    return {\n        data,\n        loading,\n        loaded,\n        isChanged,\n        load: () => { loadData(); }\n    };\n}\n\nexport function useEvent(element: Node, eventName: string, listener: (e: any) => any, deps: any[] = []) {\n    useEffect(() => {\n        element.addEventListener(eventName, listener);\n        return () => element.removeEventListener(eventName, listener);\n        \/\/ eslint-disable-next-line react-hooks\/exhaustive-deps\n    }, deps);\n}\n\nexport function useEventRef(refElement: React.RefObject&lt;any>, eventName: string, listener: (e: any) => any, deps: any[] = []) {\n    useEffect(() => {\n        const element = refElement?.current;\n        if (element) {\n            element.addEventListener(eventName, listener);\n            return () => element.removeEventListener(eventName, listener);\n        }\n        \/\/ eslint-disable-next-line react-hooks\/exhaustive-deps\n    }, deps);\n}\n\nexport const useIsMobile = (): boolean => {\n    const bp = 768;\n    const [isMobile, setIsMobile] = useState(window.innerWidth &lt; bp);\n    useEffect(() => {\n        const updateSize = (windowSize: number): void => {\n            setIsMobile(windowSize &lt; bp);\n        };\n\n        const resizeEvent = fromEvent(window, \"resize\");\n        resizeEvent.pipe(\n            map(x => (x.target as Window).innerWidth),\n            debounceTime(500)\n        ).subscribe(updateSize);\n    }, []);\n    return isMobile;\n};\n\nexport function useSubject&lt;T>(subject: Subject&lt;T | undefined>, initialValue?: T): [T | undefined, (val: T | undefined) => any] {\n    const [data, setData] = useState&lt;T | undefined>(initialValue);\n    useEffect(() => {\n        if (!subject) { return; }\n        const subscription = subject.subscribe((x: any) => {\n            setData(x);\n        });\n        return () => subscription.unsubscribe();\n    }, [subject]);\n    function updateData(val: T | undefined) {\n        subject.next(val);\n    }\n    return [data, updateData];\n}\ntype ChangeFn&lt;T> = (next: T | undefined) => any;\ntype SubscribeFn&lt;T> = (nextFn: ChangeFn&lt;T>) => IEventSubscription;\n\nexport function useSubscription&lt;T>(subscribeFn: SubscribeFn&lt;T>, deps: any[] = []): T | undefined {\n    const [data, setData] = useState&lt;T | undefined>(undefined);\n    useEffect(() => {\n        function onChange(next: T | undefined) {\n            setData(next);\n        }\n        const subscription = subscribeFn(onChange);\n        return () => subscription.unsubscribe();\n    }, deps);\/\/ eslint-disable-line react-hooks\/exhaustive-deps\n    return data;\n}\n\nexport function useAppSubscribe&lt;T>(event: EventNames, deps: any[] = []): T | undefined {\n    return useSubscription&lt;T>((fn) => appSubscribe&lt;T>(event, fn), deps);\n}\n\nexport function useBehaviorSubject&lt;T>(subject: BehaviorSubject&lt;T>): T {\n    return useObservable(subject, subject.value)!;\n}\n\nexport function useObservable&lt;T>(subject: Observable&lt;T | undefined>, initialValue?: T): T | undefined {\n    const [data, setData] = useState&lt;T | undefined>(initialValue);\n    useEffect(() => {\n        if (!subject) { return; }\n        const subscription = subject.subscribe((x: any) => {\n            setData(x);\n        });\n        return () => subscription.unsubscribe();\n    }, [subject]);\n    return data;\n}\n\nexport function useSubscribable&lt;T>(subscribableObj: ISubscribable&lt;T>, onChangeFn: (next: T | undefined) => any, deps: any[] = []): void {\n    \/\/ eslint-disable-next-line react-hooks\/exhaustive-deps\n    const subscribeFn = useMemo(() => onChangeFn, deps);\n    useEffect(() => {\n        const subscription = subscribableObj.subscribe(subscribeFn);\n        return () => subscription.unsubscribe();\n    }, [subscribableObj, subscribeFn]);\n}\n\n\nexport function useElementSize(ref: React.RefObject&lt;HTMLElement>) {\n    const [size, setSize] = useState&lt;IRect>({ height: 0, width: 0 });\n    useEffect(() => {\n        const element = ref.current;\n        if (!element) return;\n        const onSizeChanged = () => {\n            if (!element) return;\n            const rect = element.getBoundingClientRect();\n            setSize(rect);\n        };\n        const observer = new (window as any).ResizeObserver(onSizeChanged);\n        observer.observe(element);\n        return () => observer.unobserve(element);\n    }, [ref]);\n    return size;\n}\n\n\nexport function useObjectState&lt;T>(initialValue: T): [T, (c: Partial&lt;T>) => any] {\n    const [item, setItem] = useState&lt;T>(initialValue);\n    const handleChange = (changes: Partial&lt;T>) => setItem(existing => ({ ...existing, ...changes }));\n    return [item, handleChange];\n}\n\nexport function useStateDebounce&lt;T>(time: number, initialValue: T): [T, (v: T) => any] {\n    const [value, setValue] = useState&lt;T>(initialValue)\n    const [values] = useState(() => new Subject&lt;T>())\n    useEffect(() => {\n        const sub = values.pipe(debounceTime(time)).subscribe(setValue)\n        return () => sub.unsubscribe()\n    }, [time, values])\n    return [value, (v: T) => values.next(v)];\n}\n\nexport function decodeStateFromString&lt;T>(stateStr: string) {\n    return JSON.parse(atob(stateStr)) as T\n}\n\nexport function encodeStateAsString&lt;T>(state: T) {\n    return btoa(JSON.stringify(state));\n}\n\n\/\/ De hook die de toestand beheert en de URL-parameters bijwerkt\nexport function useObjectStateInSearchParams&lt;T>(nameOfUrlParam = \"state\"): [T, Dispatch&lt;SetStateAction&lt;T>>] {\n    const [searchParams, setSearchParams] = useSearchParams();\n    const state = searchParams.get(nameOfUrlParam);\n    const initialProdPlanFilter = state ? decodeStateFromString&lt;T>(state) : {} as T;\n    const [value, setValue] = useState&lt;T>(initialProdPlanFilter);\n    \/\/ Effect om de URL bij te werken wanneer de toestand verandert\n    useEffect(() => {\n        const encodedState = encodeStateAsString(value);\n        searchParams.set(nameOfUrlParam, encodedState);\n        setSearchParams(searchParams, { replace: true });\n    }, [value, searchParams, setSearchParams, nameOfUrlParam]);\n\n    return [value, setValue];\n}\n\nexport function useDebounce(callback: (...args: any[]) => any, delay: number) {\n    const timeoutRef = useRef&lt;any>(null);\n\n    useEffect(() => {\n        \/\/ Cleanup the previous timeout on re-render\n        return () => {\n            if (timeoutRef.current) {\n                clearTimeout(timeoutRef.current);\n            }\n        };\n    }, []);\n\n    const debouncedCallback = (...args: any[]) => {\n        if (timeoutRef.current) {\n            clearTimeout(timeoutRef.current);\n        }\n\n        timeoutRef.current = setTimeout(() => {\n            callback(...args);\n        }, delay);\n    };\n\n    return debouncedCallback;\n}\n\nexport function formatNumberWithEuro(amount: number | null | undefined) {\n    if (amount == null || isNaN(amount)) {\n        return '\u20ac 0,00';\n    }\n\n    return new Intl.NumberFormat('nl-NL', {\n        style: 'currency',\n        currency: 'EUR',\n        minimumFractionDigits: 2,\n        maximumFractionDigits: 2,\n    }).format(amount);\n}\n\nexport function formatDateWithTime(dateString: any): string {\n    const date = getDate(dateString);\n    if (!date) return '-';\n    if (isNaN(date.getTime())) return '-'; \/\/ invalid date\n    const datePart = date.toLocaleDateString('nl-NL', { timeZone: 'UTC' });\n    const hours = date.getUTCHours();\n    const minutes = date.getUTCMinutes();\n\n    if (hours === 0 &amp;&amp; minutes === 0) {\n        return datePart; \/\/ No time if midnight\n    }\n\n    const timePart = date.toLocaleTimeString('nl-NL', {\n        hour: '2-digit',\n        minute: '2-digit',\n        timeZone: 'UTC'\n    });\n\n    return `${datePart} ${timePart}`;\n}\n\n\/\/ threshold, between 0 and 1, 1 is fully visible on screen.\nexport function useIsVisible(\n    ref: RefObject&lt;HTMLElement>,\n    threshold: number = 0.01 \/\/ default: zodra iets in beeld is\n) {\n    const [isVisible, setIsVisible] = useState(false);\n\n    const observer = useMemo(() => {\n        return new IntersectionObserver(\n            ([entry]) => {\n                setIsVisible(entry.intersectionRatio >= threshold);\n            },\n            { threshold }\n        );\n    }, [threshold]);\n\n    useEffect(() => {\n        if (!ref.current) return;\n\n        const current = ref.current;\n        observer.observe(current);\n\n        return () => {\n            observer.unobserve(current);\n            observer.disconnect();\n        };\n    }, [observer, ref]);\n\n    return isVisible;\n}\n\n\/\/ threshold, between 0 and 1, 1 is fully visible on screen.\nexport function useVisibleThresholdVertical(ref: RefObject&lt;HTMLElement>) {\n    const thresholdObservableValue = useMemo(() => makeMut(0), []);\n\n    const observer = useMemo(() => {\n        return new IntersectionObserver(\n            ([entry]) => {\n                const intersectionRatioY = entry.intersectionRect.height \/ entry.boundingClientRect.height;\n                \/\/entry.intersectionRatio\n                thresholdObservableValue.setValue(_ => intersectionRatioY);\n            },\n            {\n                root: document.body,\n                threshold: 1, \/\/ 0.00 \u2192 1.00\n            }\n        );\n    }, [thresholdObservableValue]);\n    useEffect(() => {\n        if (!ref.current) return;\n\n        const current = ref.current;\n        observer.observe(current);\n\n        return () => {\n            observer.unobserve(current);\n            observer.disconnect();\n        };\n    }, [observer, ref]);\n\n    return thresholdObservableValue;\n}\n\nexport function parseToDate(dateLike: unknown): Date | null {\n    if (!dateLike) return null;\n    if (dateLike instanceof Date) return dateLike;\n    const parsed = new Date(dateLike as string | number);\n    return isNaN(parsed.getTime()) ? null : parsed;\n}\n\nexport function getDutchMonthName(date: Date | null): string {\n    if (!date) return \"\";\n    return new Intl.DateTimeFormat('nl-NL', { month: 'long' }).format(date);\n}\n\nexport function getFirstDayOfPreviousMonth(date: Date = new Date()): Date {\n    return new Date(date.getFullYear(), date.getMonth() - 1, 1);\n}\n\nexport function useLocalStorageWithEvents(key: string, initialValue: any, uniqueId: string) {\n    const [storedValue, setStoredValue] = useState(() => {\n        try {\n            const item = window.localStorage.getItem(key);\n            return item ? JSON.parse(item) : initialValue;\n        } catch (error) {\n            console.error(error);\n            return initialValue;\n        }\n    });\n\n    const isSettingValue = useRef(false);\n\n    useEffect(() => {\n        const handleLocalStorageChange = (data: any) => {\n            \/\/ Controleer of het event van een andere bron komt\n            if (data.key === key &amp;&amp; data.uniqueId === uniqueId &amp;&amp; !isSettingValue.current) {\n                setStoredValue(data.newValue);\n            }\n        };\n\n        const subscription = appSubscribe(\"localStorageChange\", handleLocalStorageChange);\n\n        return () => {\n            subscription.unsubscribe();\n        };\n    }, [key, uniqueId]);\n\n    const setValue = (value: any) => {\n        try {\n            const valueToStore = value instanceof Function ? value(storedValue) : value;\n            isSettingValue.current = true;\n            setStoredValue(valueToStore);\n            window.localStorage.setItem(key, JSON.stringify(valueToStore));\n            isSettingValue.current = false;\n\n            \/\/ Emit the event with uniqueId\n            appEmit(EventNames.localStorageChange, { key, newValue: valueToStore, uniqueId });\n        } catch (error) {\n            console.error(error);\n        }\n    };\n\n    return [storedValue, setValue];\n}\n\nexport interface IDestroyable { destroy: () => any }\nexport interface IModalInstance { showModel: (fn: (ref: IDestroyable) => any) => void }\n\nexport function useAppModel(): IModalInstance {\n    const app = App.useApp();\n    function showModel(fn: (ref: IDestroyable) => any) {\n        let model: IDestroyable | undefined = undefined;\n        const destroyable: IDestroyable = {\n            destroy: () => model?.destroy()\n        }\n        model = app.modal.success(fn(destroyable));\n    }\n    return {\n        showModel: showModel\n    };\n}\n\n\n\n\/**\n * Geeft de zichtbare verticale ratio van het element binnen de container.\n * @returns Een waarde tussen 0 en 1 (0 = niet zichtbaar, 1 = volledig zichtbaar)\n *\/\nexport function visibleThresholdVertical(el: HTMLElement, containerRect: DOMRect): number {\n    const elRect = el.getBoundingClientRect();\n\n    const visibleTop = Math.max(elRect.top, containerRect.top);\n    const visibleBottom = Math.min(elRect.bottom, containerRect.bottom);\n\n    const visibleHeight = Math.max(0, visibleBottom - visibleTop);\n    const elHeight = elRect.height;\n\n    if (elHeight === 0) return 0; \/\/ Voorkom delen door 0\n\n    const visibleRatio = visibleHeight \/ elHeight;\n    return Math.min(1, Math.max(0, visibleRatio)); \/\/ Clamp naar [0, 1]\n}\n\nexport interface IVisibleChecker {\n    visibleThresholdVertical(e: HTMLElement): number;\n}\n\nexport function useVisibleCheckerForRef(ref: RefObject&lt;HTMLElement>) {\n    const visibleChecker = useMemo(() => makeMut&lt;IVisibleChecker>({ visibleThresholdVertical: (_) => 0 }), []);\n    useEffect(() => {\n        if (!ref.current) return;\n        const e = ref.current;\n\n        function onScroll() {\n            const containerRect = e.getBoundingClientRect();\n            const isVisible = (el: HTMLElement) => visibleThresholdVertical(el, containerRect);\n            visibleChecker.setValue(_ => ({ visibleThresholdVertical: isVisible }));\n        }\n\n        e.addEventListener(\"scroll\", onScroll, { passive: true });\n        const observer = new (window as any).ResizeObserver(onScroll);\n        observer.observe(e);\n\n        onScroll();\n        return () => {\n            e.removeEventListener(\"scroll\", onScroll);\n            observer.unobserve(e);\n        };\n    }, [ref, visibleChecker]);\n    return visibleChecker;\n}\n\nexport function useLatest&lt;T>(value: T) {\n    const ref = useRef(value);\n    useEffect(() => {\n        ref.current = value;\n    }, [value]);\n    return ref;\n}\n\n\/**\n * Hook to extract the 'id' parameter from the URL.\n * Returns undefined if the id is 'new', otherwise returns the id as string.\n * This allows for creating new items (POST) vs editing existing items (GET).\n *\/\nexport function useIdParam(): string | undefined {\n    const { id } = useParams&lt;{ id: string }>();\n    return id === 'new' ? undefined : id;\n}<\/pre><\/div>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hooks Components NPM packages Forms More helper code<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"inline_featured_image":false,"footnotes":""},"class_list":["post-9265","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/pages\/9265","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/comments?post=9265"}],"version-history":[{"count":17,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/pages\/9265\/revisions"}],"predecessor-version":[{"id":9869,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/pages\/9265\/revisions\/9869"}],"wp:attachment":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/media?parent=9265"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}