# react-utils/loading

Queue async tasks with loading state tracking.

```typescript
import {
    LoadingContextProvider,
    useLoadingStatus,
    useAddLoadingTask,
    useLoadingEffect,
    useLoadingMemo,
} from '@yaoc1996/js-tools/react-utils'
```

---

## Provider

---

### `LoadingContextProvider`

Wraps children with a loading context. Must be an ancestor of any component that calls the loading hooks.

#### Example

```typescript
function App() {
    return (
        <LoadingContextProvider>
            <MyPage />
        </LoadingContextProvider>
    )
}
```

---

## Hooks

---

### `useLoadingStatus()`

Reads the current loading state and returns a setter for `progress` and `message`.

#### Returns

`[{ isLoading, progress, message }, setLoadingStatus]` — the current status tuple and a stable partial-merge updater.

#### Example

```typescript
const [{ isLoading, progress, message }, setStatus] = useLoadingStatus()

// Update progress during a task
setStatus({ progress: 0.5, message: 'Loading tiles…' })
```

#### Notes

- `setLoadingStatus` is stable across re-renders (memoised with `useCallback`).
- Only `progress` and `message` are writable via this hook. `isLoading` is managed by `useAddLoadingTask`.

---

### `useAddLoadingTask()`

Returns a `loadTask(task, options?)` function that enqueues an async task through the shared `TaskManager`, setting `isLoading=true` on start and `isLoading=false` on finish. Errors are caught and logged; loading always clears.

#### Returns

A `loadTask` function with signature `(task: Task, options?: TaskOptions) => () => void`.

The returned value from `loadTask` is a **stop function** — calling it cancels the task if it has not yet started, or triggers its destructor if it has.

#### Example

```typescript
const loadTask = useAddLoadingTask()

const stop = loadTask(async () => {
    const data = await fetchData()
    setData(data)

    return () => {
        // optional destructor, called by stop()
        cleanup()
    }
}, { minimumElapsedTime: 300 })

// Cancel or trigger destructor
stop()
```

#### Notes

- Tasks are run sequentially (FIFO) via the shared `TaskManager`.
- `minimumElapsedTime` (ms) keeps the loading indicator visible for at least that duration, even if the task finishes faster.
- On start, `progress` and `message` are reset to `0` and `null`.
- On finish, `isLoading` and `progress` are reset to `false` and `0`.

---

### `useLoadingEffect(getTask, deps, options?)`

Combines `useEffect` + `useAddLoadingTask`. Calls `getTask()` on mount and whenever `deps` change, enqueues the returned task, and returns the stop function as the effect cleanup.

#### Parameters

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `getTask` | `() => Task` | — | Factory called on each effect run. Return a falsy value to skip. |
| `deps` | `DependencyList` | — | Dependency array passed to `useEffect`. |
| `options` | `TaskOptions` | `{}` | Optional `{ minimumElapsedTime }`. |

#### Example

```typescript
useLoadingEffect(() => async () => {
    const data = await fetchData(id)
    setData(data)
}, [id])
```

#### Notes

- The effect cleanup calls the stop function, which triggers the task's destructor on unmount or re-run.
- `getTask` is not in the dependency array — use `deps` to control re-runs.

---

### `useLoadingMemo<T>(getCreator, deps, options?)`

Like `useLoadingEffect` but tracks the async-computed value in local state. Returns `null` on initial render and retains the previous value while reloading.

#### Parameters

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `getCreator` | `() => () => Promise<T>` | — | Factory returning the async creator function. |
| `deps` | `DependencyList` | — | Dependency array; re-runs the creator when changed. |
| `options` | `TaskOptions` | `{}` | Optional `{ minimumElapsedTime }`. |

#### Returns

`T | null` — `null` until the first task resolves, then the computed value.

#### Example

```typescript
const tiles = useLoadingMemo(() => async () => {
    return await loadTiles(viewport)
}, [viewport])

if (tiles === null) return <Spinner />
return <Map tiles={tiles} />
```

#### Notes

- State is not reset to `null` on reload — the previous value is retained until the new task resolves.
- `getCreator` is captured at effect-registration time (stale closure intentional); use `deps` to trigger re-runs with fresh data.

---

## Types

```typescript
type TaskDestructor = (() => void | Promise<void>) | void
type Task = () => TaskDestructor | Promise<TaskDestructor>
type TaskOptions = {
    minimumElapsedTime?: number | undefined
}
```
