functype-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/dist/async/TaskState.d.ts +24 -0
- package/dist/async/TaskState.js +1 -0
- package/dist/async/index.d.ts +25 -0
- package/dist/async/index.js +2 -0
- package/dist/async/index.js.map +1 -0
- package/dist/async/useTask.d.ts +26 -0
- package/dist/async/useTask.js +2 -0
- package/dist/async/useTask.js.map +1 -0
- package/dist/async/useTaskPromise.d.ts +16 -0
- package/dist/async/useTaskPromise.js +2 -0
- package/dist/async/useTaskPromise.js.map +1 -0
- package/dist/async/useTaskValue.d.ts +23 -0
- package/dist/async/useTaskValue.js +2 -0
- package/dist/async/useTaskValue.js.map +1 -0
- package/dist/eq-DRsa9bDd.d.ts +30 -0
- package/dist/forms/Validated.d.ts +22 -0
- package/dist/forms/Validated.js +2 -0
- package/dist/forms/Validated.js.map +1 -0
- package/dist/forms/index.d.ts +4 -0
- package/dist/forms/index.js +1 -0
- package/dist/forms/useValidatedField.d.ts +32 -0
- package/dist/forms/useValidatedField.js +2 -0
- package/dist/forms/useValidatedField.js.map +1 -0
- package/dist/forms/useValidatedForm.d.ts +38 -0
- package/dist/forms/useValidatedForm.js +2 -0
- package/dist/forms/useValidatedForm.js.map +1 -0
- package/dist/hooks/eq.d.ts +2 -0
- package/dist/hooks/eq.js +2 -0
- package/dist/hooks/eq.js.map +1 -0
- package/dist/hooks/index.d.ts +10 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useEither.d.ts +2 -0
- package/dist/hooks/useEither.js +2 -0
- package/dist/hooks/useEither.js.map +1 -0
- package/dist/hooks/useList.d.ts +2 -0
- package/dist/hooks/useList.js +2 -0
- package/dist/hooks/useList.js.map +1 -0
- package/dist/hooks/useOption.d.ts +2 -0
- package/dist/hooks/useOption.js +2 -0
- package/dist/hooks/useOption.js.map +1 -0
- package/dist/hooks/useStableCallback.d.ts +2 -0
- package/dist/hooks/useStableCallback.js +2 -0
- package/dist/hooks/useStableCallback.js.map +1 -0
- package/dist/hooks/useStableEffect.d.ts +2 -0
- package/dist/hooks/useStableEffect.js +2 -0
- package/dist/hooks/useStableEffect.js.map +1 -0
- package/dist/hooks/useStableMemo.d.ts +2 -0
- package/dist/hooks/useStableMemo.js +2 -0
- package/dist/hooks/useStableMemo.js.map +1 -0
- package/dist/hooks/useStableState.d.ts +2 -0
- package/dist/hooks/useStableState.js +2 -0
- package/dist/hooks/useStableState.js.map +1 -0
- package/dist/hooks/useTry.d.ts +2 -0
- package/dist/hooks/useTry.js +2 -0
- package/dist/hooks/useTry.js.map +1 -0
- package/dist/index-zHf9iM1m.d.ts +73 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -0
- package/dist/match/index.d.ts +2 -0
- package/dist/match/index.js +1 -0
- package/dist/match-imuIisms.js +2 -0
- package/dist/match-imuIisms.js.map +1 -0
- package/dist/useEither-CNHDdbY0.d.ts +15 -0
- package/dist/useList-DDjSWoK3.d.ts +18 -0
- package/dist/useOption-zK_MCd_z.d.ts +16 -0
- package/dist/useStableCallback-C4--2sM1.d.ts +12 -0
- package/dist/useStableEffect-DrXoz7gH.d.ts +16 -0
- package/dist/useStableMemo-2dH3cBpb.d.ts +13 -0
- package/dist/useStableState-yar7Urk-.d.ts +15 -0
- package/dist/useTry-CQb5RL6r.d.ts +15 -0
- package/package.json +101 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jordan Burke
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# functype-react
|
|
2
|
+
|
|
3
|
+
React bindings for the [functype](../functype) functional programming library — ADT-aware hooks and exhaustive pattern matching components.
|
|
4
|
+
|
|
5
|
+
## Thesis
|
|
6
|
+
|
|
7
|
+
Push the same ADTs (`Option`, `Either`, `Try`, `Task`, `Validated`) you already trust on the server-side into React component boundaries, so design/requirement errors fail compilation in the UI layer instead of leaking through as `data && !error && !loading` flag soup.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add functype functype-react react react-dom
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`react-dom` is an optional peer (drop it for React Native / RSC-only consumers).
|
|
16
|
+
|
|
17
|
+
## Surface
|
|
18
|
+
|
|
19
|
+
| Subpath | Contents |
|
|
20
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| `functype-react` (main) | Stable hooks (`useStable*`), ADT hooks (`useOption`, `useEither`, `useTry`, `useList`), `Match` family components, equality helpers |
|
|
22
|
+
| `functype-react/match` | `<Match>`, `<MatchOption>`, `<MatchEither>`, `<MatchTry>` (also re-exported from main) |
|
|
23
|
+
| `functype-react/async` | `useTask`, `useTaskPromise`, `useTaskValue` (React 19 `use()` bridge), `<TaskBoundary>` |
|
|
24
|
+
| `functype-react/forms` | `Validated<E, A>` type alias, `useValidatedField`, `useValidatedForm` |
|
|
25
|
+
|
|
26
|
+
`./async` and `./forms` stay off the main entry so consumers who don't touch Suspense or applicative forms tree-shake them out.
|
|
27
|
+
|
|
28
|
+
## Tier 1 — stable hooks + ADT hooks
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { useStableState, useStableEffect, structuralEq } from "functype-react"
|
|
32
|
+
|
|
33
|
+
const [user, setUser] = useStableState({ id: 1, name: "ada" }, structuralEq)
|
|
34
|
+
|
|
35
|
+
useStableEffect(
|
|
36
|
+
() => {
|
|
37
|
+
// only re-runs when user is *structurally* different
|
|
38
|
+
},
|
|
39
|
+
[user],
|
|
40
|
+
[structuralEq],
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { useOption, useEither, useTry } from "functype-react"
|
|
46
|
+
|
|
47
|
+
const userOpt = useOption<User>() // value: Option<User>
|
|
48
|
+
const result = useEither<Error, User>() // value: Either<Error, User>
|
|
49
|
+
const parsed = useTry<Config>() // value: Try<Config>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Tier 2 — pattern matching in JSX
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { Match, MatchOption } from "functype-react"
|
|
56
|
+
|
|
57
|
+
<MatchOption value={user}
|
|
58
|
+
Some={(u) => <Profile user={u} />}
|
|
59
|
+
None={() => <SignIn />}
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
<Match value={state}>
|
|
63
|
+
{{
|
|
64
|
+
Loading: () => <Spinner />,
|
|
65
|
+
Success: ({ data }) => <Result data={data} />,
|
|
66
|
+
Failure: ({ error }) => <Err err={error} />,
|
|
67
|
+
}}
|
|
68
|
+
</Match>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Omitting a `_tag` case is a compile error.
|
|
72
|
+
|
|
73
|
+
## Tier 3 — async / Task
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { useTask } from "functype-react/async"
|
|
77
|
+
|
|
78
|
+
function UserPanel({ id }: { id: string }) {
|
|
79
|
+
const state = useTask((signal) => fetch(`/users/${id}`, { signal }).then((r) => r.json()), [id])
|
|
80
|
+
if (state.isPending) return <Spinner />
|
|
81
|
+
if (state.isFailure) return <Err err={state.error} />
|
|
82
|
+
return state.isSuccess ? <Profile user={state.value} /> : null
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For React 19 `use()` + Suspense:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { TaskBoundary, useTaskValue } from "functype-react/async"
|
|
90
|
+
|
|
91
|
+
function UserPanel({ id }: { id: string }) {
|
|
92
|
+
const user = useTaskValue((signal) => fetch(`/users/${id}`, { signal }).then((r) => r.json()), [id])
|
|
93
|
+
return <Profile user={user} />
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
;<TaskBoundary pending={<Spinner />} fallback={(err, reset) => <ErrorPanel err={err} onRetry={reset} />}>
|
|
97
|
+
<UserPanel id="42" />
|
|
98
|
+
</TaskBoundary>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`useTaskValue` requires React 19. See the JSDoc on the hook for invariant documentation (stable promise refs, ErrorBoundary outside Suspense, no SSR).
|
|
102
|
+
|
|
103
|
+
## Tier 4 — forms with accumulating validation
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { useValidatedForm, valid, invalid, type Validated } from "functype-react/forms"
|
|
107
|
+
import { List } from "functype"
|
|
108
|
+
|
|
109
|
+
type SignupForm = { email: string; age: number }
|
|
110
|
+
|
|
111
|
+
const validate = (s: SignupForm): Validated<string, SignupForm> => {
|
|
112
|
+
const errs = List<string>([])
|
|
113
|
+
.concat(s.email.includes("@") ? List([]) : List(["email must contain @"]))
|
|
114
|
+
.concat(s.age >= 18 ? List([]) : List(["age must be 18+"]))
|
|
115
|
+
return errs.isEmpty ? valid(s) : invalid(errs)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function Signup() {
|
|
119
|
+
const form = useValidatedForm<SignupForm>({
|
|
120
|
+
initial: { email: "", age: 0 },
|
|
121
|
+
validate,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<form onSubmit={form.handleSubmit(async (s) => api.signup(s))}>
|
|
126
|
+
<input value={form.values.email} onChange={(e) => form.setField("email", e.target.value)} />
|
|
127
|
+
<input type="number" value={form.values.age} onChange={(e) => form.setField("age", Number(e.target.value))} />
|
|
128
|
+
{form.errors.toArray().map((err, i) => <p key={i}>{err}</p>)}
|
|
129
|
+
<button disabled={!form.isValid}>sign up</button>
|
|
130
|
+
</form>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Errors accumulate applicatively — every failing rule is surfaced in one pass, not just the first.
|
|
136
|
+
|
|
137
|
+
## Compatibility
|
|
138
|
+
|
|
139
|
+
- **TypeScript**: `strict: true` + `noUncheckedIndexedAccess: true`. Loose configs will silently lose the type-level exhaustiveness guarantees.
|
|
140
|
+
- **React**: peer dep range `>=18 <20`. Tier 3's `useTaskValue` (and consequently anything that depends on React 19's `use()` hook) is React-19-only at runtime; the rest of the package works on both.
|
|
141
|
+
- **SSR / RSC**: hooks are client-only and marked with `"use client"`. `<Match>` family components are pure and render fine in Server Components.
|
|
142
|
+
|
|
143
|
+
## Deferred to v0.2
|
|
144
|
+
|
|
145
|
+
- `./optics` subpath (`useLens`, `useOptional`, `useSelector`) — blocked on core not shipping a lens module yet.
|
|
146
|
+
- React-specific ESLint rules (`must-fold-on-component-return`, `no-getOrThrow-in-render`, etc.) — land in `eslint-functype@2.4.0` once the API stabilizes.
|
|
147
|
+
- Codemods, Storybook, cookbook recipes on the Astro site.
|
|
148
|
+
- Playwright browser-based testing for `useTaskValue` + `<TaskBoundary>` (jsdom doesn't unsuspend React 19's `use()` reliably).
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT — see [LICENSE](../../LICENSE).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/async/TaskState.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Lifecycle state of an async operation managed by `useTask`.
|
|
4
|
+
*
|
|
5
|
+
* Note: the tag values are `Idle | Pending | Success | Failure` — these are
|
|
6
|
+
* the consumer-facing lifecycle phases, intentionally broader than functype
|
|
7
|
+
* core's `TaskOutcome<T>` (which has only `Ok | Err`). The hook maps
|
|
8
|
+
* `TaskOutcome.Ok → Success`, `TaskOutcome.Err → Failure`, and adds `Idle`
|
|
9
|
+
* (pre-mount) and `Pending` (in flight) for the rendering side.
|
|
10
|
+
*/
|
|
11
|
+
type TaskState<E, A> = {
|
|
12
|
+
readonly _tag: "Idle";
|
|
13
|
+
} | {
|
|
14
|
+
readonly _tag: "Pending";
|
|
15
|
+
} | {
|
|
16
|
+
readonly _tag: "Success";
|
|
17
|
+
readonly value: A;
|
|
18
|
+
} | {
|
|
19
|
+
readonly _tag: "Failure";
|
|
20
|
+
readonly error: E;
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { TaskState };
|
|
24
|
+
//# sourceMappingURL=TaskState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TaskState } from "./TaskState.js";
|
|
2
|
+
import { UseTaskResult, useTask } from "./useTask.js";
|
|
3
|
+
import { useTaskPromise } from "./useTaskPromise.js";
|
|
4
|
+
import { useTaskValue } from "./useTaskValue.js";
|
|
5
|
+
import { ReactElement, ReactNode } from "react";
|
|
6
|
+
|
|
7
|
+
//#region src/async/TaskBoundary.d.ts
|
|
8
|
+
type Props = {
|
|
9
|
+
readonly pending: ReactNode;
|
|
10
|
+
readonly fallback: (error: unknown, reset: () => void) => ReactNode;
|
|
11
|
+
readonly children: ReactNode;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Combines `<Suspense>` (for pending Tasks consumed via `useTaskValue`) with
|
|
15
|
+
* an ErrorBoundary that catches thrown failures. The `fallback` render prop
|
|
16
|
+
* receives the thrown value (typed `unknown` — consumers narrow) and a
|
|
17
|
+
* `reset` callback that clears the error so children can be re-attempted.
|
|
18
|
+
*
|
|
19
|
+
* The ErrorBoundary wraps the Suspense, matching React's documented rule
|
|
20
|
+
* (otherwise Suspense would catch errors instead of the boundary).
|
|
21
|
+
*/
|
|
22
|
+
declare function TaskBoundary(props: Props): ReactElement;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { TaskBoundary, type TaskState, type UseTaskResult, useTask, useTaskPromise, useTaskValue };
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{useTask as e}from"./useTask.js";import{useTaskPromise as t}from"./useTaskPromise.js";import{useTaskValue as n}from"./useTaskValue.js";import{Component as r,Suspense as i}from"react";import{jsx as a}from"react/jsx-runtime";var o=class extends r{constructor(...e){super(...e),this.state={_tag:`Ok`},this.reset=()=>{this.setState({_tag:`Ok`})}}static getDerivedStateFromError(e){return{_tag:`Errored`,error:e}}componentDidCatch(e,t){}render(){return this.state._tag===`Errored`?this.props.fallback(this.state.error,this.reset):this.props.children}};function s(e){return a(o,{fallback:e.fallback,children:a(i,{fallback:e.pending,children:e.children})})}export{s as TaskBoundary,e as useTask,t as useTaskPromise,n as useTaskValue};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/async/TaskBoundary.tsx"],"sourcesContent":["\"use client\"\n\nimport { Component, type ErrorInfo, type ReactElement, type ReactNode, Suspense } from \"react\"\n\ntype Props = {\n readonly pending: ReactNode\n readonly fallback: (error: unknown, reset: () => void) => ReactNode\n readonly children: ReactNode\n}\n\ntype State = { readonly _tag: \"Ok\" } | { readonly _tag: \"Errored\"; readonly error: unknown }\n\nclass TaskErrorBoundary extends Component<\n { readonly fallback: Props[\"fallback\"]; readonly children: ReactNode },\n State\n> {\n override state: State = { _tag: \"Ok\" }\n\n static getDerivedStateFromError(error: unknown): State {\n return { _tag: \"Errored\", error }\n }\n\n override componentDidCatch(_error: unknown, _info: ErrorInfo): void {\n // hook for telemetry; intentionally noop in v0.1\n }\n\n reset = (): void => {\n this.setState({ _tag: \"Ok\" })\n }\n\n override render(): ReactNode {\n if (this.state._tag === \"Errored\") {\n return this.props.fallback(this.state.error, this.reset)\n }\n return this.props.children\n }\n}\n\n/**\n * Combines `<Suspense>` (for pending Tasks consumed via `useTaskValue`) with\n * an ErrorBoundary that catches thrown failures. The `fallback` render prop\n * receives the thrown value (typed `unknown` — consumers narrow) and a\n * `reset` callback that clears the error so children can be re-attempted.\n *\n * The ErrorBoundary wraps the Suspense, matching React's documented rule\n * (otherwise Suspense would catch errors instead of the boundary).\n */\nexport function TaskBoundary(props: Props): ReactElement {\n return (\n <TaskErrorBoundary fallback={props.fallback}>\n <Suspense fallback={props.pending}>{props.children}</Suspense>\n </TaskErrorBoundary>\n )\n}\n"],"mappings":"qOAYA,IAAM,EAAN,cAAgC,CAG9B,+BACA,KAAS,MAAe,CAAE,KAAM,KAAM,CAUtC,KAAA,UAAoB,CAClB,KAAK,SAAS,CAAE,KAAM,KAAM,CAAC,EAT/B,OAAO,yBAAyB,EAAuB,CACrD,MAAO,CAAE,KAAM,UAAW,QAAO,CAGnC,kBAA2B,EAAiB,EAAwB,EAQpE,QAA6B,CAI3B,OAHI,KAAK,MAAM,OAAS,UACf,KAAK,MAAM,SAAS,KAAK,MAAM,MAAO,KAAK,MAAM,CAEnD,KAAK,MAAM,WAatB,SAAgB,EAAa,EAA4B,CACvD,OACE,EAAC,EAAD,CAAmB,SAAU,EAAM,kBACjC,EAAC,EAAD,CAAU,SAAU,EAAM,iBAAU,EAAM,SAAoB,CAAA,CAC5C,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { TaskState } from "./TaskState.js";
|
|
2
|
+
import { Throwable } from "functype";
|
|
3
|
+
|
|
4
|
+
//#region src/async/useTask.d.ts
|
|
5
|
+
type UseTaskResult<E, A> = TaskState<E, A> & {
|
|
6
|
+
readonly isIdle: boolean;
|
|
7
|
+
readonly isPending: boolean;
|
|
8
|
+
readonly isSuccess: boolean;
|
|
9
|
+
readonly isFailure: boolean;
|
|
10
|
+
refetch: () => void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Run an async operation tied to a React component's lifecycle.
|
|
14
|
+
*
|
|
15
|
+
* - Returns a discriminated `TaskState<Throwable, A>` plus boolean flags and
|
|
16
|
+
* a `refetch` trigger.
|
|
17
|
+
* - The `task` callback receives an `AbortSignal` wired to a cancellation
|
|
18
|
+
* token that fires when the component unmounts or `deps` change. Pass the
|
|
19
|
+
* signal to `fetch` (or any abortable API) to cancel in-flight work.
|
|
20
|
+
* - StrictMode-safe: the cleanup fn cancels the token and discards any
|
|
21
|
+
* late-arriving result.
|
|
22
|
+
*/
|
|
23
|
+
declare function useTask<A>(task: (signal: AbortSignal) => Promise<A> | A, deps: ReadonlyArray<unknown>): UseTaskResult<Throwable, A>;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { UseTaskResult, useTask };
|
|
26
|
+
//# sourceMappingURL=useTask.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{useCallback as e,useEffect as t,useReducer as n,useRef as r}from"react";import{Task as i,createCancellationTokenSource as a}from"functype";function o(e,t){return t.type===`PENDING`?{_tag:`Pending`}:t.type===`SUCCESS`?{_tag:`Success`,value:t.value}:{_tag:`Failure`,error:t.error}}function s(s,c){let[l,u]=n(o,{_tag:`Idle`}),[d,f]=n(e=>e+1,0),p=r(s);p.current=s,t(()=>{let e={value:!1},t=a();return u({type:`PENDING`}),i().Async(()=>p.current(t.token.signal),void 0,void 0,t.token).then(t=>{e.value||(t.isOk()?u({type:`SUCCESS`,value:t.value}):u({type:`FAILURE`,error:t.error}))}),()=>{e.value=!0,t.cancel()}},[...c,d]);let m=e(()=>f(),[]);return{...l,isIdle:l._tag===`Idle`,isPending:l._tag===`Pending`,isSuccess:l._tag===`Success`,isFailure:l._tag===`Failure`,refetch:m}}export{s as useTask};
|
|
2
|
+
//# sourceMappingURL=useTask.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTask.js","names":[],"sources":["../../src/async/useTask.ts"],"sourcesContent":["\"use client\"\n\nimport { createCancellationTokenSource, Task, type Throwable } from \"functype\"\nimport { useCallback, useEffect, useReducer, useRef } from \"react\"\n\nimport type { TaskState } from \"./TaskState\"\n\ntype Action<E, A> =\n | { readonly type: \"PENDING\" }\n | { readonly type: \"SUCCESS\"; readonly value: A }\n | { readonly type: \"FAILURE\"; readonly error: E }\n\nfunction reducer<E, A>(_state: TaskState<E, A>, action: Action<E, A>): TaskState<E, A> {\n if (action.type === \"PENDING\") return { _tag: \"Pending\" }\n if (action.type === \"SUCCESS\") return { _tag: \"Success\", value: action.value }\n return { _tag: \"Failure\", error: action.error }\n}\n\nexport type UseTaskResult<E, A> = TaskState<E, A> & {\n readonly isIdle: boolean\n readonly isPending: boolean\n readonly isSuccess: boolean\n readonly isFailure: boolean\n refetch: () => void\n}\n\n/**\n * Run an async operation tied to a React component's lifecycle.\n *\n * - Returns a discriminated `TaskState<Throwable, A>` plus boolean flags and\n * a `refetch` trigger.\n * - The `task` callback receives an `AbortSignal` wired to a cancellation\n * token that fires when the component unmounts or `deps` change. Pass the\n * signal to `fetch` (or any abortable API) to cancel in-flight work.\n * - StrictMode-safe: the cleanup fn cancels the token and discards any\n * late-arriving result.\n */\nexport function useTask<A>(\n task: (signal: AbortSignal) => Promise<A> | A,\n deps: ReadonlyArray<unknown>,\n): UseTaskResult<Throwable, A> {\n const [state, dispatch] = useReducer(reducer<Throwable, A>, { _tag: \"Idle\" })\n const [refetchTick, forceRefetch] = useReducer((n: number) => n + 1, 0)\n const taskRef = useRef(task)\n taskRef.current = task\n\n useEffect(() => {\n const cancelled = { value: false }\n const tokenSource = createCancellationTokenSource()\n dispatch({ type: \"PENDING\" })\n\n void Task<A>()\n .Async<A>(() => taskRef.current(tokenSource.token.signal) as Promise<A>, undefined, undefined, tokenSource.token)\n .then((outcome) => {\n if (cancelled.value) return\n if (outcome.isOk()) {\n dispatch({ type: \"SUCCESS\", value: outcome.value as A })\n } else {\n dispatch({ type: \"FAILURE\", error: outcome.error as Throwable })\n }\n })\n\n return () => {\n cancelled.value = true\n tokenSource.cancel()\n }\n }, [...deps, refetchTick])\n\n const refetch = useCallback(() => forceRefetch(), [])\n\n return {\n ...state,\n isIdle: state._tag === \"Idle\",\n isPending: state._tag === \"Pending\",\n isSuccess: state._tag === \"Success\",\n isFailure: state._tag === \"Failure\",\n refetch,\n }\n}\n"],"mappings":"+JAYA,SAAS,EAAc,EAAyB,EAAuC,CAGrF,OAFI,EAAO,OAAS,UAAkB,CAAE,KAAM,UAAW,CACrD,EAAO,OAAS,UAAkB,CAAE,KAAM,UAAW,MAAO,EAAO,MAAO,CACvE,CAAE,KAAM,UAAW,MAAO,EAAO,MAAO,CAsBjD,SAAgB,EACd,EACA,EAC6B,CAC7B,GAAM,CAAC,EAAO,GAAY,EAAW,EAAuB,CAAE,KAAM,OAAQ,CAAC,CACvE,CAAC,EAAa,GAAgB,EAAY,GAAc,EAAI,EAAG,EAAE,CACjE,EAAU,EAAO,EAAK,CAC5B,EAAQ,QAAU,EAElB,MAAgB,CACd,IAAM,EAAY,CAAE,MAAO,GAAO,CAC5B,EAAc,GAA+B,CAcnD,OAbA,EAAS,CAAE,KAAM,UAAW,CAAC,CAE7B,GAAc,CACX,UAAe,EAAQ,QAAQ,EAAY,MAAM,OAAO,CAAgB,IAAA,GAAW,IAAA,GAAW,EAAY,MAAM,CAChH,KAAM,GAAY,CACb,EAAU,QACV,EAAQ,MAAM,CAChB,EAAS,CAAE,KAAM,UAAW,MAAO,EAAQ,MAAY,CAAC,CAExD,EAAS,CAAE,KAAM,UAAW,MAAO,EAAQ,MAAoB,CAAC,GAElE,KAES,CACX,EAAU,MAAQ,GAClB,EAAY,QAAQ,GAErB,CAAC,GAAG,EAAM,EAAY,CAAC,CAE1B,IAAM,EAAU,MAAkB,GAAc,CAAE,EAAE,CAAC,CAErD,MAAO,CACL,GAAG,EACH,OAAQ,EAAM,OAAS,OACvB,UAAW,EAAM,OAAS,UAC1B,UAAW,EAAM,OAAS,UAC1B,UAAW,EAAM,OAAS,UAC1B,UACD"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TaskOutcome } from "functype";
|
|
2
|
+
|
|
3
|
+
//#region src/async/useTaskPromise.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Returns a `Promise<TaskOutcome<A>>` that is stable across renders with the
|
|
6
|
+
* same `deps`. Intended for consumers of React 19's `use()` hook — providing
|
|
7
|
+
* a new promise reference on each render would infinite-suspend.
|
|
8
|
+
*
|
|
9
|
+
* The `task` callback is read through a ref, so the latest closure is invoked
|
|
10
|
+
* even if it isn't included in `deps`. Encode in `deps` whatever semantically
|
|
11
|
+
* changes the task's result.
|
|
12
|
+
*/
|
|
13
|
+
declare function useTaskPromise<A>(task: (signal: AbortSignal) => Promise<A> | A, deps: ReadonlyArray<unknown>): Promise<TaskOutcome<A>>;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { useTaskPromise };
|
|
16
|
+
//# sourceMappingURL=useTaskPromise.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{useMemo as e,useRef as t}from"react";import{Task as n,createCancellationTokenSource as r}from"functype";function i(i,a){let o=t(i);return o.current=i,e(()=>{let e=r();return n().Async(()=>o.current(e.token.signal),void 0,void 0,e.token)},a)}export{i as useTaskPromise};
|
|
2
|
+
//# sourceMappingURL=useTaskPromise.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTaskPromise.js","names":[],"sources":["../../src/async/useTaskPromise.ts"],"sourcesContent":["\"use client\"\n\nimport { createCancellationTokenSource, Task, type TaskOutcome } from \"functype\"\nimport { useMemo, useRef } from \"react\"\n\n/**\n * Returns a `Promise<TaskOutcome<A>>` that is stable across renders with the\n * same `deps`. Intended for consumers of React 19's `use()` hook — providing\n * a new promise reference on each render would infinite-suspend.\n *\n * The `task` callback is read through a ref, so the latest closure is invoked\n * even if it isn't included in `deps`. Encode in `deps` whatever semantically\n * changes the task's result.\n */\nexport function useTaskPromise<A>(\n task: (signal: AbortSignal) => Promise<A> | A,\n deps: ReadonlyArray<unknown>,\n): Promise<TaskOutcome<A>> {\n const taskRef = useRef(task)\n taskRef.current = task\n\n return useMemo(() => {\n const tokenSource = createCancellationTokenSource()\n return Task<A>().Async<A>(\n () => taskRef.current(tokenSource.token.signal) as Promise<A>,\n undefined,\n undefined,\n tokenSource.token,\n )\n }, deps)\n}\n"],"mappings":"4HAcA,SAAgB,EACd,EACA,EACyB,CACzB,IAAM,EAAU,EAAO,EAAK,CAG5B,MAFA,GAAQ,QAAU,EAEX,MAAc,CACnB,IAAM,EAAc,GAA+B,CACnD,OAAO,GAAS,CAAC,UACT,EAAQ,QAAQ,EAAY,MAAM,OAAO,CAC/C,IAAA,GACA,IAAA,GACA,EAAY,MACb,EACA,EAAK"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/async/useTaskValue.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Suspense-aware variant of `useTask` for React 19+. Suspends the component
|
|
4
|
+
* while the task is pending and throws on failure — both behaviors integrate
|
|
5
|
+
* with `<TaskBoundary>` (or a hand-rolled Suspense + ErrorBoundary pair).
|
|
6
|
+
*
|
|
7
|
+
* Invariants enforced by React 19:
|
|
8
|
+
* 1. The promise must be stable across renders. `useTaskPromise` memoizes by
|
|
9
|
+
* `deps`, so passing the same deps yields the same promise.
|
|
10
|
+
* 2. An ErrorBoundary must wrap the Suspense, never the reverse — otherwise
|
|
11
|
+
* Suspense will catch the thrown error instead of the boundary.
|
|
12
|
+
* 3. Do not call this on the server. Pass the underlying `Task` (or its
|
|
13
|
+
* promise) into a Client Component and call `useTaskValue` there.
|
|
14
|
+
*
|
|
15
|
+
* Testing note: React 19's `use()` does not unsuspend reliably under jsdom +
|
|
16
|
+
* @testing-library/react. End-to-end tests of `useTaskValue` + `<TaskBoundary>`
|
|
17
|
+
* require a real browser scheduler (Playwright); unit tests of `useTask` and
|
|
18
|
+
* `useTaskPromise` cover the Task plumbing.
|
|
19
|
+
*/
|
|
20
|
+
declare function useTaskValue<A>(task: (signal: AbortSignal) => Promise<A> | A, deps: ReadonlyArray<unknown>): A;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { useTaskValue };
|
|
23
|
+
//# sourceMappingURL=useTaskValue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTaskValue.js","names":[],"sources":["../../src/async/useTaskValue.ts"],"sourcesContent":["\"use client\"\n/* eslint-disable functype/prefer-either -- the throw is the contract: React's `use()` semantics require errors to propagate to the nearest ErrorBoundary via throw, not via an Either return. */\n\nimport { use } from \"react\"\n\nimport { useTaskPromise } from \"./useTaskPromise\"\n\n/**\n * Suspense-aware variant of `useTask` for React 19+. Suspends the component\n * while the task is pending and throws on failure — both behaviors integrate\n * with `<TaskBoundary>` (or a hand-rolled Suspense + ErrorBoundary pair).\n *\n * Invariants enforced by React 19:\n * 1. The promise must be stable across renders. `useTaskPromise` memoizes by\n * `deps`, so passing the same deps yields the same promise.\n * 2. An ErrorBoundary must wrap the Suspense, never the reverse — otherwise\n * Suspense will catch the thrown error instead of the boundary.\n * 3. Do not call this on the server. Pass the underlying `Task` (or its\n * promise) into a Client Component and call `useTaskValue` there.\n *\n * Testing note: React 19's `use()` does not unsuspend reliably under jsdom +\n * @testing-library/react. End-to-end tests of `useTaskValue` + `<TaskBoundary>`\n * require a real browser scheduler (Playwright); unit tests of `useTask` and\n * `useTaskPromise` cover the Task plumbing.\n */\nexport function useTaskValue<A>(task: (signal: AbortSignal) => Promise<A> | A, deps: ReadonlyArray<unknown>): A {\n const outcome = use(useTaskPromise(task, deps))\n if (outcome.isErr()) {\n throw outcome.error\n }\n return outcome.value as A\n}\n"],"mappings":"8FAyBA,SAAgB,EAAgB,EAA+C,EAAiC,CAC9G,IAAM,EAAU,EAAI,EAAe,EAAM,EAAK,CAAC,CAC/C,GAAI,EAAQ,OAAO,CACjB,MAAM,EAAQ,MAEhB,OAAO,EAAQ"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/hooks/eq.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Equality comparators for use with `useStable*` hooks.
|
|
4
|
+
*
|
|
5
|
+
* functype core doesn't ship an Eq typeclass; instead, comparators are passed
|
|
6
|
+
* explicitly. The default everywhere is `referenceEq` (Object.is), matching
|
|
7
|
+
* React's built-in dependency comparison.
|
|
8
|
+
*/
|
|
9
|
+
type Eq<A> = (a: A, b: A) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Reference equality, identical to React's default `Object.is` for hook deps.
|
|
12
|
+
*/
|
|
13
|
+
declare const referenceEq: Eq<unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* Compares two functype ADTs by their `_tag` literal only.
|
|
16
|
+
*
|
|
17
|
+
* Useful when you only care about variant changes (e.g., re-render when an
|
|
18
|
+
* `Option` flips between Some and None, but not on payload changes). Returns
|
|
19
|
+
* false for non-tagged values.
|
|
20
|
+
*/
|
|
21
|
+
declare const tagEq: Eq<unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Recursive structural equality. Handles primitives, arrays, plain objects, and
|
|
24
|
+
* tagged ADTs. Cycles are not detected — passing cyclic structures is undefined
|
|
25
|
+
* behavior. Functions compare by reference.
|
|
26
|
+
*/
|
|
27
|
+
declare const structuralEq: Eq<unknown>;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { tagEq as i, referenceEq as n, structuralEq as r, Eq as t };
|
|
30
|
+
//# sourceMappingURL=eq-DRsa9bDd.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Either, List } from "functype";
|
|
2
|
+
|
|
3
|
+
//#region src/forms/Validated.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Applicative-error-accumulating result for form validation.
|
|
6
|
+
*
|
|
7
|
+
* Encoded as `Either<List<E>, A>` — the same shape Scala's cats uses for
|
|
8
|
+
* `ValidatedNel`. Multiple field errors accumulate via `List.concat`; on
|
|
9
|
+
* success, the validated value rides in the `Right` channel.
|
|
10
|
+
*
|
|
11
|
+
* functype core already exposes this pattern via `FormValidation<T>` in
|
|
12
|
+
* `packages/functype/src/error/typed/Validation.ts`; this alias just gives
|
|
13
|
+
* the React-facing surface an ergonomic name.
|
|
14
|
+
*/
|
|
15
|
+
type Validated<E, A> = Either<List<E>, A>;
|
|
16
|
+
/** Construct a `Valid` (Right) result. */
|
|
17
|
+
declare function valid<A>(value: A): Validated<never, A>;
|
|
18
|
+
/** Construct an `Invalid` (Left) result from an iterable of errors. */
|
|
19
|
+
declare function invalid<E>(errors: List<E> | ReadonlyArray<E>): Validated<E, never>;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { Validated, invalid, valid };
|
|
22
|
+
//# sourceMappingURL=Validated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Validated.js","names":[],"sources":["../../src/forms/Validated.ts"],"sourcesContent":["import { type Either, Left, List, type List as ListT, Right } from \"functype\"\n\n/**\n * Applicative-error-accumulating result for form validation.\n *\n * Encoded as `Either<List<E>, A>` — the same shape Scala's cats uses for\n * `ValidatedNel`. Multiple field errors accumulate via `List.concat`; on\n * success, the validated value rides in the `Right` channel.\n *\n * functype core already exposes this pattern via `FormValidation<T>` in\n * `packages/functype/src/error/typed/Validation.ts`; this alias just gives\n * the React-facing surface an ergonomic name.\n */\nexport type Validated<E, A> = Either<ListT<E>, A>\n\n/** Construct a `Valid` (Right) result. */\nexport function valid<A>(value: A): Validated<never, A> {\n return Right<ListT<never>, A>(value)\n}\n\n/** Construct an `Invalid` (Left) result from an iterable of errors. */\nexport function invalid<E>(errors: ListT<E> | ReadonlyArray<E>): Validated<E, never> {\n const list = errors instanceof Array ? List<E>(errors) : (errors as ListT<E>)\n return Left<ListT<E>, never>(list)\n}\n"],"mappings":"qDAgBA,SAAgB,EAAS,EAA+B,CACtD,OAAO,EAAuB,EAAM,CAItC,SAAgB,EAAW,EAA0D,CAEnF,OAAO,EADM,aAAkB,MAAQ,EAAQ,EAAO,CAAI,EACxB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{invalid as e,valid as t}from"./Validated.js";import{useValidatedField as n}from"./useValidatedField.js";import{useValidatedForm as r}from"./useValidatedForm.js";export{e as invalid,n as useValidatedField,r as useValidatedForm,t as valid};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Validated } from "./Validated.js";
|
|
2
|
+
import { ChangeEvent } from "react";
|
|
3
|
+
import { List } from "functype";
|
|
4
|
+
|
|
5
|
+
//#region src/forms/useValidatedField.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Single-field state container with continuously-derived validation.
|
|
8
|
+
*
|
|
9
|
+
* `validation` is re-derived from `validate(value)` on every render — keep
|
|
10
|
+
* `validate` cheap or memoize the callback.
|
|
11
|
+
*
|
|
12
|
+
* `bind()` returns props ready to spread onto a native `<input>`. The
|
|
13
|
+
* generic `A` may be any value type; `bind` is most useful when `A` is
|
|
14
|
+
* string-shaped. For non-string fields, wire `value`/`setValue` directly.
|
|
15
|
+
*/
|
|
16
|
+
declare function useValidatedField<A, E = string>(opts: {
|
|
17
|
+
readonly initial: A;
|
|
18
|
+
readonly validate: (a: A) => Validated<E, A>;
|
|
19
|
+
}): {
|
|
20
|
+
readonly value: A;
|
|
21
|
+
setValue: (a: A) => void;
|
|
22
|
+
readonly validation: Validated<E, A>;
|
|
23
|
+
readonly errors: List<E>;
|
|
24
|
+
readonly isValid: boolean;
|
|
25
|
+
bind: () => {
|
|
26
|
+
value: A;
|
|
27
|
+
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { useValidatedField };
|
|
32
|
+
//# sourceMappingURL=useValidatedField.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{useCallback as e,useMemo as t,useState as n}from"react";import{List as r}from"functype";function i(i){let[a,o]=n(i.initial),s=t(()=>i.validate(a),[i,a]);return{value:a,setValue:o,validation:s,errors:t(()=>s.fold(e=>e,()=>r.empty()),[s]),isValid:s.isRight(),bind:e(()=>({value:a,onChange:e=>o(e.target.value)}),[a])}}export{i as useValidatedField};
|
|
2
|
+
//# sourceMappingURL=useValidatedField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useValidatedField.js","names":[],"sources":["../../src/forms/useValidatedField.ts"],"sourcesContent":["\"use client\"\n\nimport { List, type List as ListT } from \"functype\"\nimport { type ChangeEvent, useCallback, useMemo, useState } from \"react\"\n\nimport type { Validated } from \"./Validated\"\n\n/**\n * Single-field state container with continuously-derived validation.\n *\n * `validation` is re-derived from `validate(value)` on every render — keep\n * `validate` cheap or memoize the callback.\n *\n * `bind()` returns props ready to spread onto a native `<input>`. The\n * generic `A` may be any value type; `bind` is most useful when `A` is\n * string-shaped. For non-string fields, wire `value`/`setValue` directly.\n */\nexport function useValidatedField<A, E = string>(opts: {\n readonly initial: A\n readonly validate: (a: A) => Validated<E, A>\n}): {\n readonly value: A\n setValue: (a: A) => void\n readonly validation: Validated<E, A>\n readonly errors: ListT<E>\n readonly isValid: boolean\n bind: () => { value: A; onChange: (e: ChangeEvent<HTMLInputElement>) => void }\n} {\n const [value, setValue] = useState<A>(opts.initial)\n\n const validation = useMemo(() => opts.validate(value), [opts, value])\n const errors = useMemo(\n () =>\n validation.fold(\n (es) => es,\n () => List.empty<E>(),\n ),\n [validation],\n )\n const isValid = validation.isRight()\n\n const bind = useCallback(\n () => ({\n value,\n onChange: (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value as unknown as A),\n }),\n [value],\n )\n\n return { value, setValue, validation, errors, isValid, bind }\n}\n"],"mappings":"4GAiBA,SAAgB,EAAiC,EAU/C,CACA,GAAM,CAAC,EAAO,GAAY,EAAY,EAAK,QAAQ,CAE7C,EAAa,MAAc,EAAK,SAAS,EAAM,CAAE,CAAC,EAAM,EAAM,CAAC,CAmBrE,MAAO,CAAE,QAAO,WAAU,aAAY,OAlBvB,MAEX,EAAW,KACR,GAAO,MACF,EAAK,OAAU,CACtB,CACH,CAAC,EAAW,CAY8B,CAAE,QAV9B,EAAW,SAU0B,CAAE,KAR1C,OACJ,CACL,QACA,SAAW,GAAqC,EAAS,EAAE,OAAO,MAAsB,CACzF,EACD,CAAC,EAAM,CAGkD,CAAE"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Validated } from "./Validated.js";
|
|
2
|
+
import { FormEvent } from "react";
|
|
3
|
+
import { List } from "functype";
|
|
4
|
+
|
|
5
|
+
//#region src/forms/useValidatedForm.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Form-level state container with applicative error accumulation.
|
|
8
|
+
*
|
|
9
|
+
* The consumer-provided `validate` is the canonical place to accumulate
|
|
10
|
+
* field-level errors. A common pattern:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const validate = (s: Form): Validated<string, Form> => {
|
|
14
|
+
* const errs = List<string>([])
|
|
15
|
+
* .concat(s.email.includes("@") ? List([]) : List(["email must contain @"]))
|
|
16
|
+
* .concat(s.age >= 18 ? List([]) : List(["age must be 18+"]))
|
|
17
|
+
* return errs.isEmpty ? valid(s) : invalid(errs)
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* `handleSubmit(onValid)` returns an event handler that calls `e.preventDefault()`
|
|
22
|
+
* and only invokes `onValid` when the current form value passes validation.
|
|
23
|
+
*/
|
|
24
|
+
declare function useValidatedForm<S extends Record<string, unknown>, E = string>(opts: {
|
|
25
|
+
readonly initial: S;
|
|
26
|
+
readonly validate: (s: S) => Validated<E, S>;
|
|
27
|
+
}): {
|
|
28
|
+
readonly values: S;
|
|
29
|
+
setField: <K extends keyof S>(key: K, value: S[K]) => void;
|
|
30
|
+
readonly validation: Validated<E, S>;
|
|
31
|
+
readonly errors: List<E>;
|
|
32
|
+
readonly isValid: boolean;
|
|
33
|
+
reset: () => void;
|
|
34
|
+
handleSubmit: (onValid: (s: S) => void | Promise<void>) => (e: FormEvent) => void;
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
export { useValidatedForm };
|
|
38
|
+
//# sourceMappingURL=useValidatedForm.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{useCallback as e,useMemo as t,useState as n}from"react";import{List as r}from"functype";function i(i){let[a,o]=n(i.initial),s=e((e,t)=>{o(n=>({...n,[e]:t}))},[]),c=e(()=>o(i.initial),[i.initial]),l=t(()=>i.validate(a),[i,a]);return{values:a,setField:s,validation:l,errors:t(()=>l.fold(e=>e,()=>r.empty()),[l]),isValid:l.isRight(),reset:c,handleSubmit:e(e=>t=>{t.preventDefault(),l.fold(()=>void 0,t=>{e(t)})},[l])}}export{i as useValidatedForm};
|
|
2
|
+
//# sourceMappingURL=useValidatedForm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useValidatedForm.js","names":[],"sources":["../../src/forms/useValidatedForm.ts"],"sourcesContent":["\"use client\"\n\nimport { List, type List as ListT } from \"functype\"\nimport { type FormEvent, useCallback, useMemo, useState } from \"react\"\n\nimport type { Validated } from \"./Validated\"\n\n/**\n * Form-level state container with applicative error accumulation.\n *\n * The consumer-provided `validate` is the canonical place to accumulate\n * field-level errors. A common pattern:\n *\n * ```ts\n * const validate = (s: Form): Validated<string, Form> => {\n * const errs = List<string>([])\n * .concat(s.email.includes(\"@\") ? List([]) : List([\"email must contain @\"]))\n * .concat(s.age >= 18 ? List([]) : List([\"age must be 18+\"]))\n * return errs.isEmpty ? valid(s) : invalid(errs)\n * }\n * ```\n *\n * `handleSubmit(onValid)` returns an event handler that calls `e.preventDefault()`\n * and only invokes `onValid` when the current form value passes validation.\n */\nexport function useValidatedForm<S extends Record<string, unknown>, E = string>(opts: {\n readonly initial: S\n readonly validate: (s: S) => Validated<E, S>\n}): {\n readonly values: S\n setField: <K extends keyof S>(key: K, value: S[K]) => void\n readonly validation: Validated<E, S>\n readonly errors: ListT<E>\n readonly isValid: boolean\n reset: () => void\n handleSubmit: (onValid: (s: S) => void | Promise<void>) => (e: FormEvent) => void\n} {\n const [values, setValues] = useState<S>(opts.initial)\n\n const setField = useCallback(<K extends keyof S>(key: K, value: S[K]) => {\n setValues((prev) => ({ ...prev, [key]: value }))\n }, [])\n\n const reset = useCallback(() => setValues(opts.initial), [opts.initial])\n\n const validation = useMemo(() => opts.validate(values), [opts, values])\n const errors = useMemo(\n () =>\n validation.fold(\n (es) => es,\n () => List.empty<E>(),\n ),\n [validation],\n )\n const isValid = validation.isRight()\n\n const handleSubmit = useCallback(\n (onValid: (s: S) => void | Promise<void>) => (e: FormEvent) => {\n e.preventDefault()\n validation.fold(\n () => undefined,\n (s) => {\n void onValid(s)\n },\n )\n },\n [validation],\n )\n\n return { values, setField, validation, errors, isValid, reset, handleSubmit }\n}\n"],"mappings":"4GAyBA,SAAgB,EAAgE,EAW9E,CACA,GAAM,CAAC,EAAQ,GAAa,EAAY,EAAK,QAAQ,CAE/C,EAAW,GAAgC,EAAQ,IAAgB,CACvE,EAAW,IAAU,CAAE,GAAG,GAAO,GAAM,EAAO,EAAE,EAC/C,EAAE,CAAC,CAEA,EAAQ,MAAkB,EAAU,EAAK,QAAQ,CAAE,CAAC,EAAK,QAAQ,CAAC,CAElE,EAAa,MAAc,EAAK,SAAS,EAAO,CAAE,CAAC,EAAM,EAAO,CAAC,CAwBvE,MAAO,CAAE,SAAQ,WAAU,aAAY,OAvBxB,MAEX,EAAW,KACR,GAAO,MACF,EAAK,OAAU,CACtB,CACH,CAAC,EAAW,CAiB+B,CAAE,QAf/B,EAAW,SAe2B,CAAE,QAAO,aAb1C,EAClB,GAA6C,GAAiB,CAC7D,EAAE,gBAAgB,CAClB,EAAW,SACH,IAAA,GACL,GAAM,CACL,EAAa,EAAE,EAElB,EAEH,CAAC,EAAW,CAG6D,CAAE"}
|
package/dist/hooks/eq.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=Object.is,t=(e,t)=>{if(Object.is(e,t))return!0;if(typeof e!=`object`||!e||typeof t!=`object`||!t)return!1;let n=e._tag,r=t._tag;return n!==void 0&&n===r},n=(e,t)=>{if(Object.is(e,t))return!0;if(typeof e!=typeof t||e===null||t===null||typeof e!=`object`)return!1;if(Array.isArray(e))return!Array.isArray(t)||e.length!==t.length?!1:e.every((e,r)=>n(e,t[r]));if(Array.isArray(t))return!1;let r=Object.keys(e),i=Object.keys(t);return r.length===i.length?r.every(r=>Object.prototype.hasOwnProperty.call(t,r)&&n(e[r],t[r])):!1};export{e as referenceEq,n as structuralEq,t as tagEq};
|
|
2
|
+
//# sourceMappingURL=eq.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eq.js","names":[],"sources":["../../src/hooks/eq.ts"],"sourcesContent":["/**\n * Equality comparators for use with `useStable*` hooks.\n *\n * functype core doesn't ship an Eq typeclass; instead, comparators are passed\n * explicitly. The default everywhere is `referenceEq` (Object.is), matching\n * React's built-in dependency comparison.\n */\n\nexport type Eq<A> = (a: A, b: A) => boolean\n\n/**\n * Reference equality, identical to React's default `Object.is` for hook deps.\n */\nexport const referenceEq: Eq<unknown> = Object.is\n\n/**\n * Compares two functype ADTs by their `_tag` literal only.\n *\n * Useful when you only care about variant changes (e.g., re-render when an\n * `Option` flips between Some and None, but not on payload changes). Returns\n * false for non-tagged values.\n */\nexport const tagEq: Eq<unknown> = (a, b) => {\n if (Object.is(a, b)) return true\n if (typeof a !== \"object\" || a === null) return false\n if (typeof b !== \"object\" || b === null) return false\n const ta = (a as { _tag?: unknown })._tag\n const tb = (b as { _tag?: unknown })._tag\n return ta !== undefined && ta === tb\n}\n\n/**\n * Recursive structural equality. Handles primitives, arrays, plain objects, and\n * tagged ADTs. Cycles are not detected — passing cyclic structures is undefined\n * behavior. Functions compare by reference.\n */\nexport const structuralEq: Eq<unknown> = (a, b) => {\n if (Object.is(a, b)) return true\n if (typeof a !== typeof b) return false\n if (a === null || b === null) return false\n if (typeof a !== \"object\") return false\n\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== b.length) return false\n return a.every((item, i) => structuralEq(item, b[i]))\n }\n if (Array.isArray(b)) return false\n\n const ka = Object.keys(a as object)\n const kb = Object.keys(b as object)\n if (ka.length !== kb.length) return false\n return ka.every(\n (k) =>\n Object.prototype.hasOwnProperty.call(b, k) &&\n structuralEq((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]),\n )\n}\n"],"mappings":"AAaA,MAAa,EAA2B,OAAO,GASlC,GAAsB,EAAG,IAAM,CAC1C,GAAI,OAAO,GAAG,EAAG,EAAE,CAAE,MAAO,GAE5B,GADI,OAAO,GAAM,WAAY,GACzB,OAAO,GAAM,WAAY,EAAY,MAAO,GAChD,IAAM,EAAM,EAAyB,KAC/B,EAAM,EAAyB,KACrC,OAAO,IAAO,IAAA,IAAa,IAAO,GAQvB,GAA6B,EAAG,IAAM,CACjD,GAAI,OAAO,GAAG,EAAG,EAAE,CAAE,MAAO,GAG5B,GAFI,OAAO,GAAM,OAAO,GACpB,IAAM,MAAQ,IAAM,MACpB,OAAO,GAAM,SAAU,MAAO,GAElC,GAAI,MAAM,QAAQ,EAAE,CAElB,MADI,CAAC,MAAM,QAAQ,EAAE,EAAI,EAAE,SAAW,EAAE,OAAe,GAChD,EAAE,OAAO,EAAM,IAAM,EAAa,EAAM,EAAE,GAAG,CAAC,CAEvD,GAAI,MAAM,QAAQ,EAAE,CAAE,MAAO,GAE7B,IAAM,EAAK,OAAO,KAAK,EAAY,CAC7B,EAAK,OAAO,KAAK,EAAY,CAEnC,OADI,EAAG,SAAW,EAAG,OACd,EAAG,MACP,GACC,OAAO,UAAU,eAAe,KAAK,EAAG,EAAE,EAC1C,EAAc,EAA8B,GAAK,EAA8B,GAAG,CACrF,CALmC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { i as tagEq, n as referenceEq, r as structuralEq, t as Eq } from "../eq-DRsa9bDd.js";
|
|
2
|
+
import { t as useEither } from "../useEither-CNHDdbY0.js";
|
|
3
|
+
import { t as useList } from "../useList-DDjSWoK3.js";
|
|
4
|
+
import { t as useOption } from "../useOption-zK_MCd_z.js";
|
|
5
|
+
import { t as useStableCallback } from "../useStableCallback-C4--2sM1.js";
|
|
6
|
+
import { t as useStableEffect } from "../useStableEffect-DrXoz7gH.js";
|
|
7
|
+
import { t as useStableMemo } from "../useStableMemo-2dH3cBpb.js";
|
|
8
|
+
import { t as useStableState } from "../useStableState-yar7Urk-.js";
|
|
9
|
+
import { t as useTry } from "../useTry-CQb5RL6r.js";
|
|
10
|
+
export { type Eq, referenceEq, structuralEq, tagEq, useEither, useList, useOption, useStableCallback, useStableEffect, useStableMemo, useStableState, useTry };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{referenceEq as e,structuralEq as t,tagEq as n}from"./eq.js";import{useEither as r}from"./useEither.js";import{useList as i}from"./useList.js";import{useOption as a}from"./useOption.js";import{useStableCallback as o}from"./useStableCallback.js";import{useStableEffect as s}from"./useStableEffect.js";import{useStableMemo as c}from"./useStableMemo.js";import{useStableState as l}from"./useStableState.js";import{useTry as u}from"./useTry.js";export{e as referenceEq,t as structuralEq,n as tagEq,r as useEither,i as useList,a as useOption,o as useStableCallback,s as useStableEffect,c as useStableMemo,l as useStableState,u as useTry};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{Left as e,Right as t}from"functype/either";import{useCallback as n,useMemo as r,useState as i}from"react";function a(a){let[o,s]=i(()=>a??e(void 0)),c=n(e=>s(t(e)),[]),l=n(t=>s(e(t)),[]),u=n((e,t)=>o.fold(e,t),[o]);return r(()=>({value:o,setRight:c,setLeft:l,fold:u}),[o,c,l,u])}export{a as useEither};
|
|
2
|
+
//# sourceMappingURL=useEither.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEither.js","names":[],"sources":["../../src/hooks/useEither.ts"],"sourcesContent":["\"use client\"\n\nimport { type Either, Left, Right } from \"functype/either\"\nimport { useCallback, useMemo, useState } from \"react\"\n\n/**\n * Stateful Either container. Defaults to Left(undefined) when no initial.\n */\nexport function useEither<E, A>(\n initial?: Either<E, A>,\n): {\n readonly value: Either<E, A>\n setRight: (a: A) => void\n setLeft: (e: E) => void\n fold: <R>(onLeft: (e: E) => R, onRight: (a: A) => R) => R\n} {\n const [value, setValue] = useState<Either<E, A>>(() => initial ?? Left<E, A>(undefined as unknown as E))\n\n const setRight = useCallback((a: A) => setValue(Right<E, A>(a)), [])\n const setLeft = useCallback((e: E) => setValue(Left<E, A>(e)), [])\n\n const fold = useCallback(<R>(onLeft: (e: E) => R, onRight: (a: A) => R) => value.fold(onLeft, onRight), [value])\n\n return useMemo(() => ({ value, setRight, setLeft, fold }), [value, setRight, setLeft, fold])\n}\n"],"mappings":"8HAQA,SAAgB,EACd,EAMA,CACA,GAAM,CAAC,EAAO,GAAY,MAA6B,GAAW,EAAW,IAAA,GAA0B,CAAC,CAElG,EAAW,EAAa,GAAS,EAAS,EAAY,EAAE,CAAC,CAAE,EAAE,CAAC,CAC9D,EAAU,EAAa,GAAS,EAAS,EAAW,EAAE,CAAC,CAAE,EAAE,CAAC,CAE5D,EAAO,GAAgB,EAAqB,IAAyB,EAAM,KAAK,EAAQ,EAAQ,CAAE,CAAC,EAAM,CAAC,CAEhH,OAAO,OAAe,CAAE,QAAO,WAAU,UAAS,OAAM,EAAG,CAAC,EAAO,EAAU,EAAS,EAAK,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{useCallback as e,useMemo as t,useState as n}from"react";import{List as r}from"functype/list";function i(i){let[a,o]=n(()=>r(i??[])),s=e(e=>o(t=>t.add(e)),[]),c=e(e=>o(t=>t.remove(e)),[]),l=e(e=>o(t=>t.removeAt(e)),[]),u=e(()=>o(r.empty()),[]),d=e(e=>a.map(e),[a]),f=e(e=>a.filter(e),[a]);return t(()=>({value:a,add:s,remove:c,removeAt:l,clear:u,map:d,filter:f}),[a,s,c,l,u,d,f])}export{i as useList};
|
|
2
|
+
//# sourceMappingURL=useList.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useList.js","names":[],"sources":["../../src/hooks/useList.ts"],"sourcesContent":["\"use client\"\n\nimport { List, type List as ListT } from \"functype/list\"\nimport { useCallback, useMemo, useState } from \"react\"\n\n/**\n * Stateful immutable List container. All mutators return new state.\n */\nexport function useList<A>(initial?: readonly A[]): {\n readonly value: ListT<A>\n add: (a: A) => void\n remove: (a: A) => void\n removeAt: (index: number) => void\n clear: () => void\n map: <B>(f: (a: A) => B) => ListT<B>\n filter: (p: (a: A) => boolean) => ListT<A>\n} {\n const [value, setValue] = useState<ListT<A>>(() => List<A>(initial ?? []))\n\n const add = useCallback((a: A) => setValue((prev) => prev.add(a) as ListT<A>), [])\n const remove = useCallback((a: A) => setValue((prev) => prev.remove(a)), [])\n const removeAt = useCallback((index: number) => setValue((prev) => prev.removeAt(index)), [])\n const clear = useCallback(() => setValue(List.empty<A>()), [])\n\n const map = useCallback(<B>(f: (a: A) => B) => value.map(f), [value])\n const filter = useCallback((p: (a: A) => boolean) => value.filter(p), [value])\n\n return useMemo(\n () => ({ value, add, remove, removeAt, clear, map, filter }),\n [value, add, remove, removeAt, clear, map, filter],\n )\n}\n"],"mappings":"iHAQA,SAAgB,EAAW,EAQzB,CACA,GAAM,CAAC,EAAO,GAAY,MAAyB,EAAQ,GAAW,EAAE,CAAC,CAAC,CAEpE,EAAM,EAAa,GAAS,EAAU,GAAS,EAAK,IAAI,EAAE,CAAa,CAAE,EAAE,CAAC,CAC5E,EAAS,EAAa,GAAS,EAAU,GAAS,EAAK,OAAO,EAAE,CAAC,CAAE,EAAE,CAAC,CACtE,EAAW,EAAa,GAAkB,EAAU,GAAS,EAAK,SAAS,EAAM,CAAC,CAAE,EAAE,CAAC,CACvF,EAAQ,MAAkB,EAAS,EAAK,OAAU,CAAC,CAAE,EAAE,CAAC,CAExD,EAAM,EAAgB,GAAmB,EAAM,IAAI,EAAE,CAAE,CAAC,EAAM,CAAC,CAC/D,EAAS,EAAa,GAAyB,EAAM,OAAO,EAAE,CAAE,CAAC,EAAM,CAAC,CAE9E,OAAO,OACE,CAAE,QAAO,MAAK,SAAQ,WAAU,QAAO,MAAK,SAAQ,EAC3D,CAAC,EAAO,EAAK,EAAQ,EAAU,EAAO,EAAK,EAAO,CACnD"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{useCallback as e,useMemo as t,useState as n}from"react";import{Option as r}from"functype/option";function i(i){let[a,o]=n(()=>r(i)),s=e(e=>o(r(e)),[]),c=e(()=>o(r.none()),[]),l=e(e=>a.map(e),[a]),u=e((e,t)=>a.fold(e,t),[a]);return t(()=>({value:a,set:s,clear:c,map:l,fold:u}),[a,s,c,l,u])}export{i as useOption};
|
|
2
|
+
//# sourceMappingURL=useOption.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useOption.js","names":[],"sources":["../../src/hooks/useOption.ts"],"sourcesContent":["\"use client\"\n/* eslint-disable functype/prefer-option -- this hook's job is to convert nullable JS values into Option, so its inputs must be nullable. */\n\nimport { Option, type Option as OptionT } from \"functype/option\"\nimport { useCallback, useMemo, useState } from \"react\"\n\n/**\n * Stateful Option container. `set(null | undefined)` clears to None.\n */\nexport function useOption<A>(initial?: A): {\n readonly value: OptionT<A>\n set: (a: A | null | undefined) => void\n clear: () => void\n map: <B>(f: (a: A) => B) => OptionT<B>\n fold: <R>(onNone: () => R, onSome: (a: A) => R) => R\n} {\n const [value, setValue] = useState<OptionT<A>>(() => Option<A>(initial))\n\n const set = useCallback((a: A | null | undefined) => setValue(Option<A>(a)), [])\n const clear = useCallback(() => setValue(Option.none<A>()), [])\n\n const map = useCallback(<B>(f: (a: A) => B) => value.map(f), [value])\n const fold = useCallback(<R>(onNone: () => R, onSome: (a: A) => R) => value.fold(onNone, onSome), [value])\n\n return useMemo(() => ({ value, set, clear, map, fold }), [value, set, clear, map, fold])\n}\n"],"mappings":"qHASA,SAAgB,EAAa,EAM3B,CACA,GAAM,CAAC,EAAO,GAAY,MAA2B,EAAU,EAAQ,CAAC,CAElE,EAAM,EAAa,GAA4B,EAAS,EAAU,EAAE,CAAC,CAAE,EAAE,CAAC,CAC1E,EAAQ,MAAkB,EAAS,EAAO,MAAS,CAAC,CAAE,EAAE,CAAC,CAEzD,EAAM,EAAgB,GAAmB,EAAM,IAAI,EAAE,CAAE,CAAC,EAAM,CAAC,CAC/D,EAAO,GAAgB,EAAiB,IAAwB,EAAM,KAAK,EAAQ,EAAO,CAAE,CAAC,EAAM,CAAC,CAE1G,OAAO,OAAe,CAAE,QAAO,MAAK,QAAO,MAAK,OAAM,EAAG,CAAC,EAAO,EAAK,EAAO,EAAK,EAAK,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{referenceEq as e}from"./eq.js";import{useCallback as t,useRef as n}from"react";function r(r,i,a){let o=n(null),s=n(0);if(o.current===null)o.current=i,s.current+=1;else{let t=o.current;i.some((n,r)=>!(a?.[r]??e)(t[r],n))&&(o.current=i,s.current+=1)}return t(r,[s.current])}export{r as useStableCallback};
|
|
2
|
+
//# sourceMappingURL=useStableCallback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStableCallback.js","names":[],"sources":["../../src/hooks/useStableCallback.ts"],"sourcesContent":["\"use client\"\n/* eslint-disable functype/prefer-option, functype/prefer-fold -- React hooks must accept idiomatic optional params and use ref-init sentinels; wrapping in Option would change the public API shape consumers expect. */\n\nimport { type DependencyList, useCallback, useRef } from \"react\"\n\nimport { type Eq, referenceEq } from \"./eq\"\n\n/**\n * Like `useCallback`, but only returns a new function when *some* dep has\n * changed under the supplied (or default) comparator.\n */\nexport function useStableCallback<F extends (...args: never[]) => unknown>(\n callback: F,\n deps: DependencyList,\n eqs?: ReadonlyArray<Eq<unknown> | undefined>,\n): F {\n const prev = useRef<DependencyList | null>(null)\n const tick = useRef(0)\n\n if (prev.current === null) {\n prev.current = deps\n tick.current += 1\n } else {\n const stale = prev.current\n const changed = deps.some((d, i) => {\n const cmp = eqs?.[i] ?? referenceEq\n return !cmp(stale[i], d)\n })\n if (changed) {\n prev.current = deps\n tick.current += 1\n }\n }\n\n return useCallback(callback, [tick.current]) as F\n}\n"],"mappings":"mGAWA,SAAgB,EACd,EACA,EACA,EACG,CACH,IAAM,EAAO,EAA8B,KAAK,CAC1C,EAAO,EAAO,EAAE,CAEtB,GAAI,EAAK,UAAY,KACnB,EAAK,QAAU,EACf,EAAK,SAAW,MACX,CACL,IAAM,EAAQ,EAAK,QACH,EAAK,MAAM,EAAG,IAErB,EADK,IAAM,IAAM,GACZ,EAAM,GAAI,EAAE,CAEf,GACT,EAAK,QAAU,EACf,EAAK,SAAW,GAIpB,OAAO,EAAY,EAAU,CAAC,EAAK,QAAQ,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{referenceEq as e}from"./eq.js";import{useEffect as t,useRef as n}from"react";function r(r,i,a){let o=n(null),s=n(0);if(o.current===null)o.current=i,s.current+=1;else{let t=o.current;i.some((n,r)=>!(a?.[r]??e)(t[r],n))&&(o.current=i,s.current+=1)}t(r,[s.current])}export{r as useStableEffect};
|
|
2
|
+
//# sourceMappingURL=useStableEffect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStableEffect.js","names":[],"sources":["../../src/hooks/useStableEffect.ts"],"sourcesContent":["\"use client\"\n/* eslint-disable functype/prefer-option, functype/prefer-fold -- React hooks must accept idiomatic optional params and use ref-init sentinels; wrapping in Option would change the public API shape consumers expect. */\n\nimport { type DependencyList, type EffectCallback, useEffect, useRef } from \"react\"\n\nimport { type Eq, referenceEq } from \"./eq\"\n\n/**\n * Like `useEffect`, but re-runs only when *some* dep has changed under the\n * supplied (or default) comparator. The `eqs` array is aligned positionally\n * with `deps`; missing entries fall back to `referenceEq`.\n *\n * Useful when deps include functype ADTs (`Option`, `Either`, etc.) whose\n * structural identity is what matters, but whose references churn every render.\n */\nexport function useStableEffect(\n effect: EffectCallback,\n deps: DependencyList,\n eqs?: ReadonlyArray<Eq<unknown> | undefined>,\n): void {\n const prev = useRef<DependencyList | null>(null)\n const tick = useRef(0)\n\n if (prev.current === null) {\n prev.current = deps\n tick.current += 1\n } else {\n const stale = prev.current\n const changed = deps.some((d, i) => {\n const cmp = eqs?.[i] ?? referenceEq\n return !cmp(stale[i], d)\n })\n if (changed) {\n prev.current = deps\n tick.current += 1\n }\n }\n\n useEffect(effect, [tick.current])\n}\n"],"mappings":"iGAeA,SAAgB,EACd,EACA,EACA,EACM,CACN,IAAM,EAAO,EAA8B,KAAK,CAC1C,EAAO,EAAO,EAAE,CAEtB,GAAI,EAAK,UAAY,KACnB,EAAK,QAAU,EACf,EAAK,SAAW,MACX,CACL,IAAM,EAAQ,EAAK,QACH,EAAK,MAAM,EAAG,IAErB,EADK,IAAM,IAAM,GACZ,EAAM,GAAI,EAAE,CAEf,GACT,EAAK,QAAU,EACf,EAAK,SAAW,GAIpB,EAAU,EAAQ,CAAC,EAAK,QAAQ,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{referenceEq as e}from"./eq.js";import{useMemo as t,useRef as n}from"react";function r(r,i,a){let o=n(null),s=n(0);if(o.current===null)o.current=i,s.current+=1;else{let t=o.current;i.some((n,r)=>!(a?.[r]??e)(t[r],n))&&(o.current=i,s.current+=1)}return t(r,[s.current])}export{r as useStableMemo};
|
|
2
|
+
//# sourceMappingURL=useStableMemo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStableMemo.js","names":[],"sources":["../../src/hooks/useStableMemo.ts"],"sourcesContent":["\"use client\"\n/* eslint-disable functype/prefer-option, functype/prefer-fold -- React hooks must accept idiomatic optional params and use ref-init sentinels; wrapping in Option would change the public API shape consumers expect. */\n\nimport { type DependencyList, useMemo, useRef } from \"react\"\n\nimport { type Eq, referenceEq } from \"./eq\"\n\n/**\n * Like `useMemo`, but recomputes only when *some* dep has changed under the\n * supplied (or default) comparator. The `eqs` array is aligned positionally\n * with `deps`; missing entries fall back to `referenceEq`.\n */\nexport function useStableMemo<A>(\n factory: () => A,\n deps: DependencyList,\n eqs?: ReadonlyArray<Eq<unknown> | undefined>,\n): A {\n const prev = useRef<DependencyList | null>(null)\n const tick = useRef(0)\n\n if (prev.current === null) {\n prev.current = deps\n tick.current += 1\n } else {\n const stale = prev.current\n const changed = deps.some((d, i) => {\n const cmp = eqs?.[i] ?? referenceEq\n return !cmp(stale[i], d)\n })\n if (changed) {\n prev.current = deps\n tick.current += 1\n }\n }\n\n return useMemo(factory, [tick.current])\n}\n"],"mappings":"+FAYA,SAAgB,EACd,EACA,EACA,EACG,CACH,IAAM,EAAO,EAA8B,KAAK,CAC1C,EAAO,EAAO,EAAE,CAEtB,GAAI,EAAK,UAAY,KACnB,EAAK,QAAU,EACf,EAAK,SAAW,MACX,CACL,IAAM,EAAQ,EAAK,QACH,EAAK,MAAM,EAAG,IAErB,EADK,IAAM,IAAM,GACZ,EAAM,GAAI,EAAE,CAEf,GACT,EAAK,QAAU,EACf,EAAK,SAAW,GAIpB,OAAO,EAAQ,EAAS,CAAC,EAAK,QAAQ,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{referenceEq as e}from"./eq.js";import{useCallback as t,useRef as n,useState as r}from"react";function i(i,a=e){let[o,s]=r(i),c=n(o);return c.current=o,[o,t(e=>{let t=typeof e==`function`?e(c.current):e;a(c.current,t)||s(t)},[a])]}export{i as useStableState};
|
|
2
|
+
//# sourceMappingURL=useStableState.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStableState.js","names":[],"sources":["../../src/hooks/useStableState.ts"],"sourcesContent":["\"use client\"\n\nimport { useCallback, useRef, useState } from \"react\"\n\nimport { type Eq, referenceEq } from \"./eq\"\n\n/**\n * Drop-in replacement for `useState` that no-ops when the next value is equal\n * to the current under `eq`. Defaults to `Object.is` (React's built-in), so the\n * behavior is identical to `useState` unless an `eq` is supplied.\n *\n * @param initial - Initial value or a lazy producer (called once)\n * @param eq - Comparator used to decide whether to skip the update\n */\nexport function useStableState<A>(\n initial: A | (() => A),\n eq: Eq<A> = referenceEq as Eq<A>,\n): readonly [A, (next: A | ((prev: A) => A)) => void] {\n const [value, setValue] = useState<A>(initial)\n const ref = useRef<A>(value)\n ref.current = value\n\n const setStable = useCallback(\n (next: A | ((prev: A) => A)) => {\n const resolved = typeof next === \"function\" ? (next as (prev: A) => A)(ref.current) : next\n if (!eq(ref.current, resolved)) {\n setValue(resolved)\n }\n },\n [eq],\n )\n\n return [value, setStable] as const\n}\n"],"mappings":"iHAcA,SAAgB,EACd,EACA,EAAY,EACwC,CACpD,GAAM,CAAC,EAAO,GAAY,EAAY,EAAQ,CACxC,EAAM,EAAU,EAAM,CAa5B,MAZA,GAAI,QAAU,EAYP,CAAC,EAVU,EACf,GAA+B,CAC9B,IAAM,EAAW,OAAO,GAAS,WAAc,EAAwB,EAAI,QAAQ,CAAG,EACjF,EAAG,EAAI,QAAS,EAAS,EAC5B,EAAS,EAAS,EAGtB,CAAC,EAAG,CAGkB,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{useCallback as e,useMemo as t,useState as n}from"react";import{Try as r}from"functype/try";function i(i){let[a,o]=n(()=>i??r.failure(Error(`uninitialized`))),s=e(e=>o(r.success(e)),[]),c=e(e=>o(r.failure(e)),[]),l=e((e,t)=>a.fold(e,t),[a]);return t(()=>({value:a,setSuccess:s,setFailure:c,fold:l}),[a,s,c,l])}export{i as useTry};
|
|
2
|
+
//# sourceMappingURL=useTry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTry.js","names":[],"sources":["../../src/hooks/useTry.ts"],"sourcesContent":["\"use client\"\n\nimport { Try, type Try as TryT } from \"functype/try\"\nimport { useCallback, useMemo, useState } from \"react\"\n\n/**\n * Stateful Try container. Defaults to Failure(new Error(\"uninitialized\")) when no initial.\n */\nexport function useTry<A>(initial?: TryT<A>): {\n readonly value: TryT<A>\n setSuccess: (a: A) => void\n setFailure: (e: Error) => void\n fold: <R>(onFailure: (e: Error) => R, onSuccess: (a: A) => R) => R\n} {\n const [value, setValue] = useState<TryT<A>>(() => initial ?? Try.failure<A>(new Error(\"uninitialized\")))\n\n const setSuccess = useCallback((a: A) => setValue(Try.success(a)), [])\n const setFailure = useCallback((e: Error) => setValue(Try.failure<A>(e)), [])\n\n const fold = useCallback(\n <R>(onFailure: (e: Error) => R, onSuccess: (a: A) => R) => value.fold(onFailure, onSuccess),\n [value],\n )\n\n return useMemo(() => ({ value, setSuccess, setFailure, fold }), [value, setSuccess, setFailure, fold])\n}\n"],"mappings":"+GAQA,SAAgB,EAAU,EAKxB,CACA,GAAM,CAAC,EAAO,GAAY,MAAwB,GAAW,EAAI,QAAe,MAAM,gBAAgB,CAAC,CAAC,CAElG,EAAa,EAAa,GAAS,EAAS,EAAI,QAAQ,EAAE,CAAC,CAAE,EAAE,CAAC,CAChE,EAAa,EAAa,GAAa,EAAS,EAAI,QAAW,EAAE,CAAC,CAAE,EAAE,CAAC,CAEvE,EAAO,GACP,EAA4B,IAA2B,EAAM,KAAK,EAAW,EAAU,CAC3F,CAAC,EAAM,CACR,CAED,OAAO,OAAe,CAAE,QAAO,aAAY,aAAY,OAAM,EAAG,CAAC,EAAO,EAAY,EAAY,EAAK,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Either } from "functype/either";
|
|
2
|
+
import { ReactElement, ReactNode } from "react";
|
|
3
|
+
import { Option } from "functype/option";
|
|
4
|
+
import { Try } from "functype/try";
|
|
5
|
+
|
|
6
|
+
//#region src/match/Match.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Maps each `_tag` literal to a handler that receives the narrowed variant.
|
|
9
|
+
*
|
|
10
|
+
* TypeScript enforces exhaustiveness: omitting a case from `children` is a
|
|
11
|
+
* compile error.
|
|
12
|
+
*/
|
|
13
|
+
type MatchCases<U extends {
|
|
14
|
+
_tag: string;
|
|
15
|
+
}> = { readonly [K in U["_tag"]]: (value: Extract<U, {
|
|
16
|
+
readonly _tag: K;
|
|
17
|
+
}>) => ReactNode };
|
|
18
|
+
/**
|
|
19
|
+
* Generic discriminated-union matcher for ADTs that follow functype's `_tag`
|
|
20
|
+
* convention. Pass a value and a record of handlers keyed on each tag; the
|
|
21
|
+
* matching handler is invoked with the narrowed variant.
|
|
22
|
+
*
|
|
23
|
+
* ```tsx
|
|
24
|
+
* <Match value={state}>
|
|
25
|
+
* {{
|
|
26
|
+
* Loading: () => <Spinner />,
|
|
27
|
+
* Success: ({ data }) => <Result data={data} />,
|
|
28
|
+
* Failure: ({ error }) => <Err err={error} />,
|
|
29
|
+
* }}
|
|
30
|
+
* </Match>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare function Match<U extends {
|
|
34
|
+
_tag: string;
|
|
35
|
+
}>(props: {
|
|
36
|
+
readonly value: U;
|
|
37
|
+
readonly children: MatchCases<U>;
|
|
38
|
+
}): ReactElement;
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/match/MatchEither.d.ts
|
|
41
|
+
/**
|
|
42
|
+
* Tag-narrowed sugar over `Match` for `Either<L, R>`.
|
|
43
|
+
*/
|
|
44
|
+
declare function MatchEither<L, R>(props: {
|
|
45
|
+
readonly value: Either<L, R>;
|
|
46
|
+
readonly Left: (l: L) => ReactNode;
|
|
47
|
+
readonly Right: (r: R) => ReactNode;
|
|
48
|
+
}): ReactElement;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/match/MatchOption.d.ts
|
|
51
|
+
/**
|
|
52
|
+
* Tag-narrowed sugar over `Match` for `Option<A>`. Renders `Some(a)` to the
|
|
53
|
+
* `Some` handler and `None` to the `None` handler.
|
|
54
|
+
*/
|
|
55
|
+
declare function MatchOption<A>(props: {
|
|
56
|
+
readonly value: Option<A>;
|
|
57
|
+
readonly Some: (a: A) => ReactNode;
|
|
58
|
+
readonly None: () => ReactNode;
|
|
59
|
+
}): ReactElement;
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/match/MatchTry.d.ts
|
|
62
|
+
/**
|
|
63
|
+
* Tag-narrowed sugar over `Match` for `Try<A>`. `Failure` receives the
|
|
64
|
+
* underlying `Error`; `Success` receives the value.
|
|
65
|
+
*/
|
|
66
|
+
declare function MatchTry<A>(props: {
|
|
67
|
+
readonly value: Try<A>;
|
|
68
|
+
readonly Success: (a: A) => ReactNode;
|
|
69
|
+
readonly Failure: (e: Error) => ReactNode;
|
|
70
|
+
}): ReactElement;
|
|
71
|
+
//#endregion
|
|
72
|
+
export { MatchCases as a, Match as i, MatchOption as n, MatchEither as r, MatchTry as t };
|
|
73
|
+
//# sourceMappingURL=index-zHf9iM1m.d.ts.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { i as tagEq, n as referenceEq, r as structuralEq, t as Eq } from "./eq-DRsa9bDd.js";
|
|
2
|
+
import { t as useEither } from "./useEither-CNHDdbY0.js";
|
|
3
|
+
import { t as useList } from "./useList-DDjSWoK3.js";
|
|
4
|
+
import { t as useOption } from "./useOption-zK_MCd_z.js";
|
|
5
|
+
import { t as useStableCallback } from "./useStableCallback-C4--2sM1.js";
|
|
6
|
+
import { t as useStableEffect } from "./useStableEffect-DrXoz7gH.js";
|
|
7
|
+
import { t as useStableMemo } from "./useStableMemo-2dH3cBpb.js";
|
|
8
|
+
import { t as useStableState } from "./useStableState-yar7Urk-.js";
|
|
9
|
+
import { t as useTry } from "./useTry-CQb5RL6r.js";
|
|
10
|
+
import { a as MatchCases, i as Match, n as MatchOption, r as MatchEither, t as MatchTry } from "./index-zHf9iM1m.js";
|
|
11
|
+
export { Eq, Match, MatchCases, MatchEither, MatchOption, MatchTry, referenceEq, structuralEq, tagEq, useEither, useList, useOption, useStableCallback, useStableEffect, useStableMemo, useStableState, useTry };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{referenceEq as e,structuralEq as t,tagEq as n}from"./hooks/eq.js";import{useEither as r}from"./hooks/useEither.js";import{useList as i}from"./hooks/useList.js";import{useOption as a}from"./hooks/useOption.js";import{useStableCallback as o}from"./hooks/useStableCallback.js";import{useStableEffect as s}from"./hooks/useStableEffect.js";import{useStableMemo as c}from"./hooks/useStableMemo.js";import{useStableState as l}from"./hooks/useStableState.js";import{useTry as u}from"./hooks/useTry.js";import"./hooks/index.js";import{i as d,n as f,r as p,t as m}from"./match-imuIisms.js";export{d as Match,p as MatchEither,f as MatchOption,m as MatchTry,e as referenceEq,t as structuralEq,n as tagEq,r as useEither,i as useList,a as useOption,o as useStableCallback,s as useStableEffect,c as useStableMemo,l as useStableState,u as useTry};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as e,n as t,r as n,t as r}from"../match-imuIisms.js";export{e as Match,n as MatchEither,t as MatchOption,r as MatchTry};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Fragment as e,jsx as t}from"react/jsx-runtime";function n(n){let r=n.children[n.value._tag];return t(e,{children:r(n.value)})}function r(n){return t(e,{children:n.value.fold(n.Left,n.Right)})}function i(n){return t(e,{children:n.value.fold(n.None,n.Some)})}function a(n){return t(e,{children:n.value.fold(n.Failure,n.Success)})}export{n as i,i as n,r,a as t};
|
|
2
|
+
//# sourceMappingURL=match-imuIisms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"match-imuIisms.js","names":[],"sources":["../src/match/Match.tsx","../src/match/MatchEither.tsx","../src/match/MatchOption.tsx","../src/match/MatchTry.tsx"],"sourcesContent":["import type { ReactElement, ReactNode } from \"react\"\n\n/**\n * Maps each `_tag` literal to a handler that receives the narrowed variant.\n *\n * TypeScript enforces exhaustiveness: omitting a case from `children` is a\n * compile error.\n */\nexport type MatchCases<U extends { _tag: string }> = {\n readonly [K in U[\"_tag\"]]: (value: Extract<U, { readonly _tag: K }>) => ReactNode\n}\n\n/**\n * Generic discriminated-union matcher for ADTs that follow functype's `_tag`\n * convention. Pass a value and a record of handlers keyed on each tag; the\n * matching handler is invoked with the narrowed variant.\n *\n * ```tsx\n * <Match value={state}>\n * {{\n * Loading: () => <Spinner />,\n * Success: ({ data }) => <Result data={data} />,\n * Failure: ({ error }) => <Err err={error} />,\n * }}\n * </Match>\n * ```\n */\nexport function Match<U extends { _tag: string }>(props: {\n readonly value: U\n readonly children: MatchCases<U>\n}): ReactElement {\n const handler = props.children[props.value._tag as U[\"_tag\"]] as (value: U) => ReactNode\n return <>{handler(props.value)}</>\n}\n","import type { Either } from \"functype/either\"\nimport type { ReactElement, ReactNode } from \"react\"\n\n/**\n * Tag-narrowed sugar over `Match` for `Either<L, R>`.\n */\nexport function MatchEither<L, R>(props: {\n readonly value: Either<L, R>\n readonly Left: (l: L) => ReactNode\n readonly Right: (r: R) => ReactNode\n}): ReactElement {\n return <>{props.value.fold(props.Left, props.Right)}</>\n}\n","import type { Option } from \"functype/option\"\nimport type { ReactElement, ReactNode } from \"react\"\n\n/**\n * Tag-narrowed sugar over `Match` for `Option<A>`. Renders `Some(a)` to the\n * `Some` handler and `None` to the `None` handler.\n */\nexport function MatchOption<A>(props: {\n readonly value: Option<A>\n readonly Some: (a: A) => ReactNode\n readonly None: () => ReactNode\n}): ReactElement {\n return <>{props.value.fold(props.None, props.Some)}</>\n}\n","import type { Try } from \"functype/try\"\nimport type { ReactElement, ReactNode } from \"react\"\n\n/**\n * Tag-narrowed sugar over `Match` for `Try<A>`. `Failure` receives the\n * underlying `Error`; `Success` receives the value.\n */\nexport function MatchTry<A>(props: {\n readonly value: Try<A>\n readonly Success: (a: A) => ReactNode\n readonly Failure: (e: Error) => ReactNode\n}): ReactElement {\n return <>{props.value.fold(props.Failure, props.Success)}</>\n}\n"],"mappings":"sDA2BA,SAAgB,EAAkC,EAGjC,CACf,IAAM,EAAU,EAAM,SAAS,EAAM,MAAM,MAC3C,OAAO,EAAA,EAAA,CAAA,SAAG,EAAQ,EAAM,MAAM,CAAI,CAAA,CC1BpC,SAAgB,EAAkB,EAIjB,CACf,OAAO,EAAA,EAAA,CAAA,SAAG,EAAM,MAAM,KAAK,EAAM,KAAM,EAAM,MAAM,CAAI,CAAA,CCJzD,SAAgB,EAAe,EAId,CACf,OAAO,EAAA,EAAA,CAAA,SAAG,EAAM,MAAM,KAAK,EAAM,KAAM,EAAM,KAAK,CAAI,CAAA,CCLxD,SAAgB,EAAY,EAIX,CACf,OAAO,EAAA,EAAA,CAAA,SAAG,EAAM,MAAM,KAAK,EAAM,QAAS,EAAM,QAAQ,CAAI,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Either } from "functype/either";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/useEither.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Stateful Either container. Defaults to Left(undefined) when no initial.
|
|
6
|
+
*/
|
|
7
|
+
declare function useEither<E, A>(initial?: Either<E, A>): {
|
|
8
|
+
readonly value: Either<E, A>;
|
|
9
|
+
setRight: (a: A) => void;
|
|
10
|
+
setLeft: (e: E) => void;
|
|
11
|
+
fold: <R>(onLeft: (e: E) => R, onRight: (a: A) => R) => R;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { useEither as t };
|
|
15
|
+
//# sourceMappingURL=useEither-CNHDdbY0.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { List } from "functype/list";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/useList.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Stateful immutable List container. All mutators return new state.
|
|
6
|
+
*/
|
|
7
|
+
declare function useList<A>(initial?: readonly A[]): {
|
|
8
|
+
readonly value: List<A>;
|
|
9
|
+
add: (a: A) => void;
|
|
10
|
+
remove: (a: A) => void;
|
|
11
|
+
removeAt: (index: number) => void;
|
|
12
|
+
clear: () => void;
|
|
13
|
+
map: <B>(f: (a: A) => B) => List<B>;
|
|
14
|
+
filter: (p: (a: A) => boolean) => List<A>;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { useList as t };
|
|
18
|
+
//# sourceMappingURL=useList-DDjSWoK3.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Option } from "functype/option";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/useOption.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Stateful Option container. `set(null | undefined)` clears to None.
|
|
6
|
+
*/
|
|
7
|
+
declare function useOption<A>(initial?: A): {
|
|
8
|
+
readonly value: Option<A>;
|
|
9
|
+
set: (a: A | null | undefined) => void;
|
|
10
|
+
clear: () => void;
|
|
11
|
+
map: <B>(f: (a: A) => B) => Option<B>;
|
|
12
|
+
fold: <R>(onNone: () => R, onSome: (a: A) => R) => R;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { useOption as t };
|
|
16
|
+
//# sourceMappingURL=useOption-zK_MCd_z.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { t as Eq } from "./eq-DRsa9bDd.js";
|
|
2
|
+
import { DependencyList } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/hooks/useStableCallback.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Like `useCallback`, but only returns a new function when *some* dep has
|
|
7
|
+
* changed under the supplied (or default) comparator.
|
|
8
|
+
*/
|
|
9
|
+
declare function useStableCallback<F extends (...args: never[]) => unknown>(callback: F, deps: DependencyList, eqs?: ReadonlyArray<Eq<unknown> | undefined>): F;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { useStableCallback as t };
|
|
12
|
+
//# sourceMappingURL=useStableCallback-C4--2sM1.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { t as Eq } from "./eq-DRsa9bDd.js";
|
|
2
|
+
import { DependencyList, EffectCallback } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/hooks/useStableEffect.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Like `useEffect`, but re-runs only when *some* dep has changed under the
|
|
7
|
+
* supplied (or default) comparator. The `eqs` array is aligned positionally
|
|
8
|
+
* with `deps`; missing entries fall back to `referenceEq`.
|
|
9
|
+
*
|
|
10
|
+
* Useful when deps include functype ADTs (`Option`, `Either`, etc.) whose
|
|
11
|
+
* structural identity is what matters, but whose references churn every render.
|
|
12
|
+
*/
|
|
13
|
+
declare function useStableEffect(effect: EffectCallback, deps: DependencyList, eqs?: ReadonlyArray<Eq<unknown> | undefined>): void;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { useStableEffect as t };
|
|
16
|
+
//# sourceMappingURL=useStableEffect-DrXoz7gH.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { t as Eq } from "./eq-DRsa9bDd.js";
|
|
2
|
+
import { DependencyList } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/hooks/useStableMemo.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Like `useMemo`, but recomputes only when *some* dep has changed under the
|
|
7
|
+
* supplied (or default) comparator. The `eqs` array is aligned positionally
|
|
8
|
+
* with `deps`; missing entries fall back to `referenceEq`.
|
|
9
|
+
*/
|
|
10
|
+
declare function useStableMemo<A>(factory: () => A, deps: DependencyList, eqs?: ReadonlyArray<Eq<unknown> | undefined>): A;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { useStableMemo as t };
|
|
13
|
+
//# sourceMappingURL=useStableMemo-2dH3cBpb.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { t as Eq } from "./eq-DRsa9bDd.js";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/useStableState.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Drop-in replacement for `useState` that no-ops when the next value is equal
|
|
6
|
+
* to the current under `eq`. Defaults to `Object.is` (React's built-in), so the
|
|
7
|
+
* behavior is identical to `useState` unless an `eq` is supplied.
|
|
8
|
+
*
|
|
9
|
+
* @param initial - Initial value or a lazy producer (called once)
|
|
10
|
+
* @param eq - Comparator used to decide whether to skip the update
|
|
11
|
+
*/
|
|
12
|
+
declare function useStableState<A>(initial: A | (() => A), eq?: Eq<A>): readonly [A, (next: A | ((prev: A) => A)) => void];
|
|
13
|
+
//#endregion
|
|
14
|
+
export { useStableState as t };
|
|
15
|
+
//# sourceMappingURL=useStableState-yar7Urk-.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Try } from "functype/try";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/useTry.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Stateful Try container. Defaults to Failure(new Error("uninitialized")) when no initial.
|
|
6
|
+
*/
|
|
7
|
+
declare function useTry<A>(initial?: Try<A>): {
|
|
8
|
+
readonly value: Try<A>;
|
|
9
|
+
setSuccess: (a: A) => void;
|
|
10
|
+
setFailure: (e: Error) => void;
|
|
11
|
+
fold: <R>(onFailure: (e: Error) => R, onSuccess: (a: A) => R) => R;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { useTry as t };
|
|
15
|
+
//# sourceMappingURL=useTry-CQb5RL6r.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "functype-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React bindings for functype — ADT-aware hooks and exhaustive pattern matching components",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"functype",
|
|
7
|
+
"react",
|
|
8
|
+
"hooks",
|
|
9
|
+
"pattern-matching",
|
|
10
|
+
"adt",
|
|
11
|
+
"option",
|
|
12
|
+
"either",
|
|
13
|
+
"task",
|
|
14
|
+
"typescript",
|
|
15
|
+
"functional"
|
|
16
|
+
],
|
|
17
|
+
"author": "jordan.burke@gmail.com",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"homepage": "https://github.com/jordanburke/functype/tree/main/packages/functype-react",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/jordanburke/functype",
|
|
23
|
+
"directory": "packages/functype-react"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": ">=18.0.0 <20.0.0",
|
|
27
|
+
"react-dom": ">=18.0.0 <20.0.0",
|
|
28
|
+
"functype": "^0.60.5"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"react-dom": {
|
|
32
|
+
"optional": true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@testing-library/dom": "^10.4.1",
|
|
37
|
+
"@testing-library/jest-dom": "^6.6.4",
|
|
38
|
+
"@testing-library/react": "^16.3.0",
|
|
39
|
+
"@types/node": "^24.12.2",
|
|
40
|
+
"@types/react": "^19.2.0",
|
|
41
|
+
"@types/react-dom": "^19.2.0",
|
|
42
|
+
"fast-check": "^4.7.0",
|
|
43
|
+
"jsdom": "^26.0.0",
|
|
44
|
+
"react": "^19.2.0",
|
|
45
|
+
"react-dom": "^19.2.0",
|
|
46
|
+
"ts-builds": "^2.8.0",
|
|
47
|
+
"tsdown": "^0.22.0",
|
|
48
|
+
"functype": "^0.60.5"
|
|
49
|
+
},
|
|
50
|
+
"type": "module",
|
|
51
|
+
"main": "./dist/index.js",
|
|
52
|
+
"module": "./dist/index.js",
|
|
53
|
+
"types": "./dist/index.d.ts",
|
|
54
|
+
"sideEffects": false,
|
|
55
|
+
"exports": {
|
|
56
|
+
".": {
|
|
57
|
+
"types": "./dist/index.d.ts",
|
|
58
|
+
"import": "./dist/index.js",
|
|
59
|
+
"default": "./dist/index.js"
|
|
60
|
+
},
|
|
61
|
+
"./match": {
|
|
62
|
+
"types": "./dist/match/index.d.ts",
|
|
63
|
+
"import": "./dist/match/index.js",
|
|
64
|
+
"default": "./dist/match/index.js"
|
|
65
|
+
},
|
|
66
|
+
"./async": {
|
|
67
|
+
"types": "./dist/async/index.d.ts",
|
|
68
|
+
"import": "./dist/async/index.js",
|
|
69
|
+
"default": "./dist/async/index.js"
|
|
70
|
+
},
|
|
71
|
+
"./forms": {
|
|
72
|
+
"types": "./dist/forms/index.d.ts",
|
|
73
|
+
"import": "./dist/forms/index.js",
|
|
74
|
+
"default": "./dist/forms/index.js"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"files": [
|
|
78
|
+
"dist"
|
|
79
|
+
],
|
|
80
|
+
"prettier": "ts-builds/prettier",
|
|
81
|
+
"engines": {
|
|
82
|
+
"node": ">=18.17.0"
|
|
83
|
+
},
|
|
84
|
+
"publishConfig": {
|
|
85
|
+
"access": "public",
|
|
86
|
+
"provenance": true
|
|
87
|
+
},
|
|
88
|
+
"scripts": {
|
|
89
|
+
"validate": "ts-builds validate",
|
|
90
|
+
"format": "ts-builds format",
|
|
91
|
+
"format:check": "ts-builds format:check",
|
|
92
|
+
"lint": "ts-builds lint",
|
|
93
|
+
"lint:check": "ts-builds lint:check",
|
|
94
|
+
"typecheck": "ts-builds typecheck",
|
|
95
|
+
"test": "ts-builds test",
|
|
96
|
+
"test:watch": "ts-builds test:watch",
|
|
97
|
+
"test:coverage": "ts-builds test:coverage",
|
|
98
|
+
"build": "ts-builds build",
|
|
99
|
+
"dev": "ts-builds dev"
|
|
100
|
+
}
|
|
101
|
+
}
|