Add useAsync hook

This commit is contained in:
Misode
2022-05-08 05:29:16 +02:00
parent 4e51d41c54
commit 2772d967e0
8 changed files with 114 additions and 63 deletions

View File

@@ -1,4 +1,6 @@
export * from './useActiveTimout'
export * from './useAsync'
export * from './useAsyncFn'
export * from './useCanvas'
export * from './useFocus'
export * from './useHash'

17
src/app/hooks/useAsync.ts Normal file
View File

@@ -0,0 +1,17 @@
import type { Inputs } from 'preact/hooks'
import { useEffect } from 'preact/hooks'
import type { AsyncState } from './useAsyncFn'
import { useAsyncFn } from './useAsyncFn'
export function useAsync<T>(
fn: () => Promise<T>,
inputs: Inputs = [],
): AsyncState<T> {
const [state, callback] = useAsyncFn<T, () => Promise<T>>(fn, inputs, { loading: true })
useEffect(() => {
callback()
}, [callback])
return state
}

View File

@@ -0,0 +1,59 @@
import type { Inputs } from 'preact/hooks'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
export type AsyncState<T> = {
loading: boolean,
error?: undefined,
value?: undefined,
} | {
loading: true,
error?: Error | undefined,
value?: T,
} | {
loading: false,
error: Error,
value?: undefined,
} | {
loading: false,
error?: undefined,
value: T,
}
export function useAsyncFn<R, T extends (...args: any[]) => Promise<R>>(
fn: T,
inputs: Inputs = [],
initialState: AsyncState<R> = { loading: false },
): [AsyncState<R>, (...args: Parameters<T>) => Promise<R | undefined>] {
const [state, setState] = useState<AsyncState<R>>(initialState)
const isMounted = useRef<boolean>(false)
const lastCallId = useRef(0)
useEffect(() => {
isMounted.current = true
return () => isMounted.current = false
}, [])
const callback = useCallback((...args: Parameters<T>): Promise<R | undefined> => {
const callId = ++lastCallId.current
if (!state.loading) {
setState(prev => ({ ...prev, loading: true }))
}
return fn(...args).then(
value => {
if (isMounted.current && callId === lastCallId.current) {
setState({ value, loading: false })
}
return value
},
error => {
if (isMounted.current && callId === lastCallId.current) {
setState({ error, loading: false })
}
return undefined
})
}, inputs)
return [state, callback]
}

View File

@@ -1,7 +1,6 @@
import { useEffect, useState } from 'preact/hooks'
import { Ad, ChangelogList, ErrorPanel } from '../components'
import { useLocale, useTitle } from '../contexts'
import type { Change } from '../services'
import { useAsync } from '../hooks'
import { getChangelogs } from '../services'
interface Props {
@@ -9,20 +8,13 @@ interface Props {
}
export function Changelog({}: Props) {
const { locale } = useLocale()
const [error, setError] = useState<Error | null>(null)
useTitle(locale('title.changelog'))
const [changelogs, setChangelogs] = useState<Change[]>([])
useEffect(() => {
getChangelogs()
.then(changelogs => setChangelogs(changelogs))
.catch(e => { console.error(e); setError(e) })
}, [])
const { value: changelogs, error } = useAsync(getChangelogs, [])
return <main>
<Ad type="text" id="changelog" />
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
{error && <ErrorPanel error={error} />}
<div class="changelog">
<ChangelogList changes={changelogs} defaultOrder="desc" />
</div>

View File

@@ -6,7 +6,7 @@ import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
import config from '../../config.json'
import { Ad, Btn, BtnMenu, ChangelogTag, Giscus, Octicon } from '../components'
import { useLocale, useTitle, useVersion } from '../contexts'
import { useActiveTimeout, useHash } from '../hooks'
import { useActiveTimeout, useAsync, useHash } from '../hooks'
import type { VersionId } from '../services'
import { parseFrontMatter, versionContent } from '../Utils'
@@ -30,7 +30,10 @@ export function Guide({ id }: Props) {
const { version, changeVersion } = useVersion()
const { changeTitle } = useTitle()
const [content, setContent] = useState<string | undefined>(undefined)
const { value: content } = useAsync(async () => {
const res = await fetch(`../../guides/${id}.md`)
return await res.text()
}, [id])
const frontMatter = useMemo(() => {
if (!content) return undefined
@@ -51,14 +54,8 @@ export function Guide({ id }: Props) {
return allowedVersions[0]
}, [version, frontMatter?.versions])
const versionedContent = useMemo(() => {
if (!content) return undefined
const guide = content.substring(content.indexOf('---', 3) + 3)
return versionContent(guide, guideVersion)
}, [guideVersion, content])
const html = useMemo(() => {
if (!versionedContent) return undefined
if (!content) return undefined
marked.use({ renderer: {
link(href, title, text) {
if (href === null) return text
@@ -72,8 +69,10 @@ export function Guide({ id }: Props) {
return `<h${level}>${link}${text}</h${level}>`
},
}})
const guide = content.substring(content.indexOf('---', 3) + 3)
const versionedContent = versionContent(guide, guideVersion)
return marked(versionedContent, { version: '1.19' } as any)
}, [versionedContent])
}, [guideVersion, content])
const [hash, setHash] = useHash()
@@ -100,14 +99,6 @@ export function Guide({ id }: Props) {
}
}, [scrollToHeading, hash, version])
useEffect(() => {
(async () => {
const res = await fetch(`../../guides/${id}.md`)
const text = await res.text()
setContent(text)
})()
}, [id])
const [shareActive, shareSuccess] = useActiveTimeout()
const onShare = useCallback(() => {
@@ -116,9 +107,9 @@ export function Guide({ id }: Props) {
shareSuccess()
}, [id, version])
const onClickTag = (tag: string) => {
const onClickTag = useCallback((tag: string) => {
route(`/guides/?tags=${tag}`)
}
}, [])
const [largeWidth] = useState(window.innerWidth > 600)

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'preact/hooks'
import { useCallback, useMemo } from 'preact/hooks'
import { Ad, TreeView } from '../components'
import { getFilePath, useLocale, useProject, useTitle } from '../contexts'
@@ -11,10 +11,10 @@ export function Project({}: Props) {
useTitle(locale('title.project', project.name))
const entries = useMemo(() => project.files.map(getFilePath), project.files)
const selectFile = (entry: string) => {
const selectFile = useCallback((entry: string) => {
const [, namespace, type, ...id] = entry.split('/')
openFile(type, `${namespace}:${id}`)
}
}, [openFile])
return <main>
<Ad id="data-pack-project" type="text" />

View File

@@ -1,9 +1,10 @@
import type { Howl, HowlOptions } from 'howler'
import { useEffect, useRef, useState } from 'preact/hooks'
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
import config from '../../config.json'
import { Btn, BtnMenu, ErrorPanel, SoundConfig, TextInput } from '../components'
import { useLocale, useTitle, useVersion } from '../contexts'
import type { SoundEvents, VersionId } from '../services'
import { useAsync } from '../hooks'
import type { VersionId } from '../services'
import { fetchSounds } from '../services'
import { hexId } from '../Utils'
@@ -13,7 +14,6 @@ interface Props {
export function Sounds({}: Props) {
const { locale } = useLocale()
const { version, changeVersion } = useVersion()
const [error, setError] = useState<Error | null>(null)
useTitle(locale('title.sounds'))
const [howler, setHowler] = useState<undefined | ((options: HowlOptions) => Howl)>(undefined)
@@ -24,13 +24,10 @@ export function Sounds({}: Props) {
})()
}, [])
const [sounds, setSounds] = useState<SoundEvents>({})
const soundKeys = Object.keys(sounds ?? {})
useEffect(() => {
fetchSounds(version)
.then(setSounds)
.catch(e => { console.error(e); setError(e) })
const { value: sounds, error } = useAsync(async () => {
return await fetchSounds(version)
}, [version])
const soundKeys = useMemo(() => Object.keys(sounds ?? {}), [sounds])
const [search, setSearch] = useState('')
const [configs, setConfigs] = useState<SoundConfig[]>([])
@@ -63,7 +60,7 @@ export function Sounds({}: Props) {
}
return <main>
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
{error && <ErrorPanel error={error} />}
{soundKeys.length > 0 && <>
<div class="controls sounds-controls">
<div class="sound-search-group">
@@ -73,7 +70,7 @@ export function Sounds({}: Props) {
</div>
{configs.length > 1 && <Btn icon="play" label={ locale('sounds.play_all')} class="play-all-sounds" onClick={playAll} />}
<div class="spacer"></div>
<Btn icon="download" label={locale('download')} tooltip={locale('sounds.download_function')} class="download-sounds" onClick={downloadFunction} />
<Btn icon="download" label={locale('download')} tooltip={locale('sounds.download_function')} tooltipLoc="se" class="download-sounds" onClick={downloadFunction} />
<BtnMenu icon="tag" label={version} tooltip={locale('switch_version')}>
{config.versions.slice().reverse().map(v =>
<Btn label={v.id} active={v.id === version} onClick={() => changeVersion(v.id as VersionId)} />
@@ -81,14 +78,14 @@ export function Sounds({}: Props) {
</BtnMenu>
</div>
<div class="sounds">
{howler && configs.map(c =>
{sounds && howler && configs.map(c =>
<SoundConfig key={c.id} {...c} {...{ howler, sounds, delayedPlay }} onEdit={editConfig(c.id)} onDelete={deleteConfig(c.id)} />
)}
</div>
<a ref={download} style="display: none;"></a>
<datalist id="sound-list">
{soundKeys.map(s => <option key={s} value={s} />)}
</datalist>
</>}
<datalist id="sound-list">
{soundKeys.map(s => <option key={s} value={s} />)}
</datalist>
</main>
}

View File

@@ -1,7 +1,6 @@
import { useEffect, useState } from 'preact/hooks'
import { Ad, ErrorPanel, Octicon, VersionDetail, VersionList } from '../components'
import { useLocale, useTitle } from '../contexts'
import { useSearchParam } from '../hooks'
import { useAsync, useSearchParam } from '../hooks'
import type { VersionMeta } from '../services'
import { fetchVersions } from '../services'
@@ -10,27 +9,21 @@ interface Props {
}
export function Versions({}: Props) {
const { locale } = useLocale()
const [error, setError] = useState<Error | null>(null)
useTitle(locale('title.versions'))
const [versions, setVersions] = useState<VersionMeta[]>([])
useEffect(() => {
fetchVersions()
.then(versions => setVersions(versions))
.catch(e => { console.error(e); setError(e) })
}, [])
const { value: versions, error } = useAsync(fetchVersions, [])
const [selectedId] = useSearchParam('id')
const selected = versions.find(v => v.id === selectedId)
const selected = (versions ?? []).find(v => v.id === selectedId)
useTitle(selected ? selected.name : 'Versions Explorer', selected ? [] : undefined)
const nextVersion = selected && getOffsetVersion(versions, selected, -1)
const previousVersion = selected && getOffsetVersion(versions, selected, 1)
const nextVersion = selected && getOffsetVersion(versions ?? [], selected, -1)
const previousVersion = selected && getOffsetVersion(versions ?? [], selected, 1)
return <main>
<Ad type="text" id="versions" />
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
{error && <ErrorPanel error={error} />}
<div class="versions">
{selectedId ? <>
<div class="navigation">
@@ -54,7 +47,7 @@ export function Versions({}: Props) {
<p>This version does not exist. Only versions since 1.14 are tracked, or it may be too recent.</p>
</div>
</div>}
</> : <VersionList versions={versions} link={id => `/versions/?id=${id}`} />}
</> : <VersionList versions={versions ?? []} link={id => `/versions/?id=${id}`} />}
</div>
</main>
}