implement usePromise hook

This commit is contained in:
Linus Miller 2025-12-13 22:43:13 +01:00
parent 7fdbef7573
commit 322d249740
12 changed files with 114 additions and 109 deletions

View File

@ -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<Account[]>([])
useEffect(() => {
rek('/api/accounts').then(setAccounts)
}, [])
const accounts = usePromise<Account[]>(() => rek('/api/accounts'))
return (
<section>

View File

@ -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,7 +16,21 @@ type Props = {
title?: string
}
const remember = throttle(function remember() {
const scroll = () => {
const offset = get(window.history, 'state.scrollTop')
window.scrollTo(0, offset || 0)
}
const App: FunctionComponent<Props> = ({ error, title }) => {
const loadRef = useRef<boolean>(false)
useEffect(() => {
history.scrollRestoration = 'manual'
const remember = throttle(function remember() {
if (loadRef.current) return
window.history.replaceState(
{
...window.history.state,
@ -25,21 +39,24 @@ const remember = throttle(function remember() {
'',
null,
)
}, 100)
}, 100)
const App: FunctionComponent<Props> = ({ error, title }) => {
useEffect(() => {
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<Props> = ({ error, title }) => {
{error ? (
<ErrorPage error={error} />
) : (
<Router onRouteChange={onRouteChange}>
<Router onLoadStart={onLoadStart} onLoadEnd={onLoadEnd} onRouteChange={onRouteChange}>
{routes.map((route) => (
<Route key={route.path} path={route.path} component={route.component} />
))}

View File

@ -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<Balance[]>([])
const [years, setYears] = useState<number[]>([])
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 (
<section>

View File

@ -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<Entry | null>(null)
const route = useRoute()
useEffect(() => {
rek(`/api/entries/${route.params.id}`).then(setEntry)
}, [])
if (!entry) return
const entry = usePromise<Entry>(() => rek(`/api/entries/${route.params.id}`))
return (
<section>

View File

@ -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<Invoice | null>(null)
const invoice = usePromise<Invoice>(() => rek(`/api/invoices/${route.params.id}`))
const route = useRoute()
useEffect(() => {
rek(`/api/invoices/${route.params.id}`).then(setInvoice)
}, [])
return (
<section>
<Head>

View File

@ -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<Supplier | null>(null)
const [invoices, setInvoices] = useState<Invoice[]>([])
const [totalAmount, setTotalAmount] = useState<number | null>(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 (
<section>

View File

@ -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<Supplier[]>([])
useEffect(() => {
rek('/api/suppliers').then(setSuppliers)
}, [])
const suppliers = usePromise<Supplier[]>(() => rek('/api/suppliers'))
return (
<section>

View File

@ -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<ObjectType[]>([])
const objects = usePromise<ObjectType[]>(() => rek('/api/objects'))
const [currentObject, setCurrentObject] = useState<ObjectType | null>(null)
useEffect(() => {
rek('/api/objects').then((objects: ObjectType[]) => setObjects(objects))
}, [])
return (
<section>
<Head>

View File

@ -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<Result[]>([])
const [years, setYears] = useState<number[]>([])
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 (
<section>

View File

@ -0,0 +1,17 @@
import { useRef, useState } from 'preact/hooks'
export default function usePromise<R = any, E = Error>(fn: () => Promise<R>) {
const [result, setResult] = useState<R | null>(null)
const [error, setError] = useState<E | null>(null)
const promiseRef = useRef<Promise<void> | 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
}
}

View File

@ -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"
},

44
pnpm-lock.yaml generated
View File

@ -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: {}