From 322d2497405cf763e30ae5ff461db33eb65783f5 Mon Sep 17 00:00:00 2001 From: Linus Miller Date: Sat, 13 Dec 2025 22:43:13 +0100 Subject: [PATCH] implement usePromise hook --- client/public/components/accounts_page.tsx | 8 +-- client/public/components/app.tsx | 53 ++++++++++++------- client/public/components/balances_page.tsx | 15 +++--- client/public/components/entry_page.tsx | 14 ++--- client/public/components/invoice_page.tsx | 13 ++--- .../components/invoices_by_supplier_page.tsx | 18 +++---- client/public/components/invoices_page.tsx | 8 +-- client/public/components/objects_page.tsx | 11 ++-- client/public/components/results_page.tsx | 20 ++++--- client/shared/hooks/use_promise.ts | 17 ++++++ package.json | 2 +- pnpm-lock.yaml | 44 +++++++-------- 12 files changed, 114 insertions(+), 109 deletions(-) create mode 100644 client/shared/hooks/use_promise.ts diff --git a/client/public/components/accounts_page.tsx b/client/public/components/accounts_page.tsx index 59211db..6d9336d 100644 --- a/client/public/components/accounts_page.tsx +++ b/client/public/components/accounts_page.tsx @@ -1,16 +1,12 @@ import { h, type FunctionComponent } from 'preact' -import { useEffect, useState } from 'preact/hooks' import rek from 'rek' import Head from './head.ts' +import usePromise from '../../shared/hooks/use_promise.ts' import type { Account } from '../../../shared/types.ts' const AccountsPage: FunctionComponent = () => { - const [accounts, setAccounts] = useState([]) - - useEffect(() => { - rek('/api/accounts').then(setAccounts) - }, []) + const accounts = usePromise(() => rek('/api/accounts')) return (
diff --git a/client/public/components/app.tsx b/client/public/components/app.tsx index f383054..01b3f33 100644 --- a/client/public/components/app.tsx +++ b/client/public/components/app.tsx @@ -1,5 +1,5 @@ import { h, type FunctionComponent } from 'preact' -import { useCallback, useEffect } from 'preact/hooks' +import { useCallback, useEffect, useRef } from 'preact/hooks' import { LocationProvider, Route, Router } from 'preact-iso' import { get } from 'lowline' import Head from './head.ts' @@ -16,30 +16,47 @@ type Props = { title?: string } -const remember = throttle(function remember() { - window.history.replaceState( - { - ...window.history.state, - scrollTop: window.scrollY, - }, - '', - null, - ) -}, 100) +const scroll = () => { + const offset = get(window.history, 'state.scrollTop') + + window.scrollTo(0, offset || 0) +} const App: FunctionComponent = ({ error, title }) => { + const loadRef = useRef(false) + useEffect(() => { + history.scrollRestoration = 'manual' + + const remember = throttle(function remember() { + if (loadRef.current) return + + window.history.replaceState( + { + ...window.history.state, + scrollTop: window.scrollY, + }, + '', + null, + ) + }, 100) + addEventListener('scroll', remember) return () => removeEventListener('scroll', remember) - }) + }, []) + + const onLoadStart = useCallback(() => { + loadRef.current = true + }, []) + + const onLoadEnd = useCallback(() => { + scroll() + loadRef.current = false + }, []) const onRouteChange = useCallback(() => { - const offset = get(window.history, 'state.scrollTop') - - setTimeout(() => { - window.scrollTo(0, offset || 0) - }, 100) + if (!loadRef.current) scroll() }, []) return ( @@ -55,7 +72,7 @@ const App: FunctionComponent = ({ error, title }) => { {error ? ( ) : ( - + {routes.map((route) => ( ))} diff --git a/client/public/components/balances_page.tsx b/client/public/components/balances_page.tsx index 1e9c39d..c673cda 100644 --- a/client/public/components/balances_page.tsx +++ b/client/public/components/balances_page.tsx @@ -1,21 +1,20 @@ import { h, type FunctionComponent } from 'preact' -import { useEffect, useState } from 'preact/hooks' import cn from 'classnames' import rek from 'rek' import Head from './head.ts' import { formatNumber } from '../utils/format_number.ts' import s from './balances_page.module.scss' +import usePromise from '../../shared/hooks/use_promise.ts' import type { Balance, FinancialYear } from '../../../shared/types.ts' const BalancesPage: FunctionComponent = () => { - const [balances, setBalances] = useState([]) - const [years, setYears] = useState([]) - - useEffect(() => { - rek(`/api/balances`).then(setBalances) - rek(`/api/financial-years`).then((financialYears: FinancialYear[]) => setYears(financialYears.map((fy) => fy.year))) - }, []) + const [balances, years] = usePromise<[Balance[], number[]]>(() => + Promise.all([ + rek(`/api/balances`), + rek(`/api/financial-years`).then((financialYears: FinancialYear[]) => financialYears.map((fy) => fy.year)), + ]), + ) return (
diff --git a/client/public/components/entry_page.tsx b/client/public/components/entry_page.tsx index 7ff51e0..e6f19b8 100644 --- a/client/public/components/entry_page.tsx +++ b/client/public/components/entry_page.tsx @@ -1,22 +1,14 @@ import { h, type FunctionComponent } from 'preact' -import { useEffect, useState } from 'preact/hooks' import { useRoute } from 'preact-iso' -import { formatNumber } from '../utils/format_number.ts' import rek from 'rek' - import { type Entry } from '../../../shared/types.ts' - +import usePromise from '../../shared/hooks/use_promise.ts' +import { formatNumber } from '../utils/format_number.ts' import Head from './head.ts' const EntryPage: FunctionComponent = () => { - const [entry, setEntry] = useState(null) const route = useRoute() - - useEffect(() => { - rek(`/api/entries/${route.params.id}`).then(setEntry) - }, []) - - if (!entry) return + const entry = usePromise(() => rek(`/api/entries/${route.params.id}`)) return (
diff --git a/client/public/components/invoice_page.tsx b/client/public/components/invoice_page.tsx index 6d6e3e0..92df624 100644 --- a/client/public/components/invoice_page.tsx +++ b/client/public/components/invoice_page.tsx @@ -1,20 +1,15 @@ import { h, type FunctionComponent } from 'preact' -import { useEffect, useState } from 'preact/hooks' import { useRoute } from 'preact-iso' import rek from 'rek' -import Head from './head.ts' -import { formatNumber } from '../utils/format_number.ts' - import type { Invoice } from '../../../shared/types.ts' +import usePromise from '../../shared/hooks/use_promise.ts' +import { formatNumber } from '../utils/format_number.ts' +import Head from './head.ts' const InvoicePage: FunctionComponent = () => { - const [invoice, setInvoice] = useState(null) + const invoice = usePromise(() => rek(`/api/invoices/${route.params.id}`)) const route = useRoute() - useEffect(() => { - rek(`/api/invoices/${route.params.id}`).then(setInvoice) - }, []) - return (
diff --git a/client/public/components/invoices_by_supplier_page.tsx b/client/public/components/invoices_by_supplier_page.tsx index 05e296f..e0ee449 100644 --- a/client/public/components/invoices_by_supplier_page.tsx +++ b/client/public/components/invoices_by_supplier_page.tsx @@ -1,26 +1,24 @@ import { h, type FunctionComponent } from 'preact' // @ts-ignore import Format from 'easy-tz/format' -import { useEffect, useState } from 'preact/hooks' import { useRoute } from 'preact-iso' import rek from 'rek' import Head from './head.ts' +import usePromise from '../../shared/hooks/use_promise.ts' import type { Invoice, Supplier } from '../../../shared/types.ts' const format = Format.bind(null, null, 'YYYY.MM.DD') const InvoicesPage: FunctionComponent = () => { - const [supplier, setSupplier] = useState(null) - const [invoices, setInvoices] = useState([]) - const [totalAmount, setTotalAmount] = useState(null) - const route = useRoute() - useEffect(() => { - rek(`/api/suppliers/${route.params.supplier}`).then(setSupplier) - rek(`/api/invoices?supplier=${route.params.supplier}`).then(setInvoices) - rek(`/api/invoices/total-amount?supplier=${route.params.supplier}`).then(setTotalAmount) - }, [route.params.supplier]) + const [supplier, invoices, totalAmount] = usePromise<[Supplier, Invoice[], number]>(() => + Promise.all([ + rek(`/api/suppliers/${route.params.supplier}`), + rek(`/api/invoices?supplier=${route.params.supplier}`), + rek(`/api/invoices/total-amount?supplier=${route.params.supplier}`), + ]), + ) return (
diff --git a/client/public/components/invoices_page.tsx b/client/public/components/invoices_page.tsx index b3be409..82520a5 100644 --- a/client/public/components/invoices_page.tsx +++ b/client/public/components/invoices_page.tsx @@ -1,16 +1,12 @@ import { h, type FunctionComponent } from 'preact' -import { useEffect, useState } from 'preact/hooks' import rek from 'rek' import Head from './head.ts' +import usePromise from '../../shared/hooks/use_promise.ts' import type { Supplier } from '../../../shared/types.ts' const InvoicesPage: FunctionComponent = () => { - const [suppliers, setSuppliers] = useState([]) - - useEffect(() => { - rek('/api/suppliers').then(setSuppliers) - }, []) + const suppliers = usePromise(() => rek('/api/suppliers')) return (
diff --git a/client/public/components/objects_page.tsx b/client/public/components/objects_page.tsx index 05c300a..b739f8f 100644 --- a/client/public/components/objects_page.tsx +++ b/client/public/components/objects_page.tsx @@ -1,19 +1,16 @@ import { h, type FunctionComponent } from 'preact' -import { useEffect, useState } from 'preact/hooks' +import { useState } from 'preact/hooks' import rek from 'rek' +import type { Object as ObjectType } from '../../../shared/types.ts' +import usePromise from '../../shared/hooks/use_promise.ts' import Head from './head.ts' import Object from './object.tsx' import s from './results_page.module.scss' -import type { Object as ObjectType } from '../../../shared/types.ts' const ObjectsPage: FunctionComponent = () => { - const [objects, setObjects] = useState([]) + const objects = usePromise(() => rek('/api/objects')) const [currentObject, setCurrentObject] = useState(null) - useEffect(() => { - rek('/api/objects').then((objects: ObjectType[]) => setObjects(objects)) - }, []) - return (
diff --git a/client/public/components/results_page.tsx b/client/public/components/results_page.tsx index 17ed784..06bc37f 100644 --- a/client/public/components/results_page.tsx +++ b/client/public/components/results_page.tsx @@ -1,21 +1,19 @@ import { h, type FunctionComponent } from 'preact' -import { useEffect, useState } from 'preact/hooks' import cn from 'classnames' import rek from 'rek' -import Head from './head.ts' +import type { FinancialYear, Result } from '../../../shared/types.ts' import { formatNumber } from '../utils/format_number.ts' +import usePromise from '../../shared/hooks/use_promise.ts' +import Head from './head.ts' import s from './results_page.module.scss' -import type { FinancialYear, Result } from '../../../shared/types.ts' - const ResultsPage: FunctionComponent = () => { - const [results, setResults] = useState([]) - const [years, setYears] = useState([]) - - useEffect(() => { - rek(`/api/results`).then((results: Result[]) => setResults(results)) - rek(`/api/financial-years`).then((financialYears: FinancialYear[]) => setYears(financialYears.map((fy) => fy.year))) - }, []) + const [results, years] = usePromise<[Result[], number[]]>(() => + Promise.all([ + rek(`/api/results`), + rek(`/api/financial-years`).then((financialYears: FinancialYear[]) => financialYears.map((fy) => fy.year)), + ]), + ) return (
diff --git a/client/shared/hooks/use_promise.ts b/client/shared/hooks/use_promise.ts new file mode 100644 index 0000000..634a376 --- /dev/null +++ b/client/shared/hooks/use_promise.ts @@ -0,0 +1,17 @@ +import { useRef, useState } from 'preact/hooks' + +export default function usePromise(fn: () => Promise) { + const [result, setResult] = useState(null) + const [error, setError] = useState(null) + const promiseRef = useRef | null>(null) + + if (!promiseRef.current) { + throw (promiseRef.current = fn().then(setResult, setError)) + } else if (result) { + return result + } else if (error) { + throw error + } else { + throw promiseRef.current + } +} diff --git a/package.json b/package.json index 3a10ac9..07b41e1 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "pg": "^8.16.3", "pg-protocol": "^1.10.3", "pino-abstract-transport": "^3.0.0", - "preact": "^10.27.2", + "preact": "^10.28.0", "preact-iso": "^2.11.0", "rek": "^0.8.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdcdac0..d662671 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,11 +60,11 @@ importers: specifier: ^3.0.0 version: 3.0.0 preact: - specifier: ^10.27.2 - version: 10.27.2 + specifier: ^10.28.0 + version: 10.28.0 preact-iso: specifier: ^2.11.0 - version: 2.11.0(preact-render-to-string@6.6.3(preact@10.27.2))(preact@10.27.2) + version: 2.11.0(preact-render-to-string@6.6.3(preact@10.28.0))(preact@10.28.0) rek: specifier: ^0.8.1 version: 0.8.1 @@ -74,10 +74,10 @@ importers: version: 7.28.5 '@preact/preset-vite': specifier: ^2.10.1 - version: 2.10.2(@babel/core@7.28.5)(preact@10.27.2)(vite@7.2.4(@types/node@24.10.1)(sass@1.94.2)(yaml@2.8.1)) + version: 2.10.2(@babel/core@7.28.5)(preact@10.28.0)(vite@7.2.4(@types/node@24.10.1)(sass@1.94.2)(yaml@2.8.1)) '@testing-library/preact': specifier: ^3.2.4 - version: 3.2.4(preact@10.27.2) + version: 3.2.4(preact@10.28.0) '@types/d3-dsv': specifier: ^3.0.7 version: 3.0.7 @@ -1895,8 +1895,8 @@ packages: peerDependencies: preact: '>=10 || >= 11.0.0-0' - preact@10.27.2: - resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} + preact@10.28.0: + resolution: {integrity: sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA==} prettier@3.6.2: resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} @@ -2830,12 +2830,12 @@ snapshots: '@pinojs/redact@0.4.0': {} - '@preact/preset-vite@2.10.2(@babel/core@7.28.5)(preact@10.27.2)(vite@7.2.4(@types/node@24.10.1)(sass@1.94.2)(yaml@2.8.1))': + '@preact/preset-vite@2.10.2(@babel/core@7.28.5)(preact@10.28.0)(vite@7.2.4(@types/node@24.10.1)(sass@1.94.2)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.5) '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.5) - '@prefresh/vite': 2.4.11(preact@10.27.2)(vite@7.2.4(@types/node@24.10.1)(sass@1.94.2)(yaml@2.8.1)) + '@prefresh/vite': 2.4.11(preact@10.28.0)(vite@7.2.4(@types/node@24.10.1)(sass@1.94.2)(yaml@2.8.1)) '@rollup/pluginutils': 4.2.1 babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.28.5) debug: 4.4.3 @@ -2848,20 +2848,20 @@ snapshots: '@prefresh/babel-plugin@0.5.2': {} - '@prefresh/core@1.5.9(preact@10.27.2)': + '@prefresh/core@1.5.9(preact@10.28.0)': dependencies: - preact: 10.27.2 + preact: 10.28.0 '@prefresh/utils@1.2.1': {} - '@prefresh/vite@2.4.11(preact@10.27.2)(vite@7.2.4(@types/node@24.10.1)(sass@1.94.2)(yaml@2.8.1))': + '@prefresh/vite@2.4.11(preact@10.28.0)(vite@7.2.4(@types/node@24.10.1)(sass@1.94.2)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 '@prefresh/babel-plugin': 0.5.2 - '@prefresh/core': 1.5.9(preact@10.27.2) + '@prefresh/core': 1.5.9(preact@10.28.0) '@prefresh/utils': 1.2.1 '@rollup/pluginutils': 4.2.1 - preact: 10.27.2 + preact: 10.28.0 vite: 7.2.4(@types/node@24.10.1)(sass@1.94.2)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -2948,10 +2948,10 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/preact@3.2.4(preact@10.27.2)': + '@testing-library/preact@3.2.4(preact@10.28.0)': dependencies: '@testing-library/dom': 8.20.1 - preact: 10.27.2 + preact: 10.28.0 '@types/aria-query@5.0.4': {} @@ -3942,16 +3942,16 @@ snapshots: dependencies: xtend: 4.0.2 - preact-iso@2.11.0(preact-render-to-string@6.6.3(preact@10.27.2))(preact@10.27.2): + preact-iso@2.11.0(preact-render-to-string@6.6.3(preact@10.28.0))(preact@10.28.0): dependencies: - preact: 10.27.2 - preact-render-to-string: 6.6.3(preact@10.27.2) + preact: 10.28.0 + preact-render-to-string: 6.6.3(preact@10.28.0) - preact-render-to-string@6.6.3(preact@10.27.2): + preact-render-to-string@6.6.3(preact@10.28.0): dependencies: - preact: 10.27.2 + preact: 10.28.0 - preact@10.27.2: {} + preact@10.28.0: {} prettier@3.6.2: {}