{"id":"fb02597c-7d1f-4d0b-9b02-f65c92e91d3e","shortId":"ryV8CJ","kind":"skill","title":"fp-ts-react","tagline":"Practical patterns for using fp-ts with React - hooks, state, forms, data fetching. Use when building React apps with functional programming patterns. Works with React 18/19, Next.js 14/15.","description":"# Functional Programming in React\n\nPractical patterns for React apps. No jargon, just code that works.\n\n## When to Use This Skill\n\n- When building React apps with fp-ts for type-safe state management\n- When handling loading/error/success states in data fetching\n- When implementing form validation with error accumulation\n- When using React 18/19 or Next.js 14/15 with functional patterns\n\n---\n\n## Quick Reference\n\n| Pattern | Use When |\n|---------|----------|\n| `Option` | Value might be missing (user not loaded yet) |\n| `Either` | Operation might fail (form validation) |\n| `TaskEither` | Async operation might fail (API calls) |\n| `RemoteData` | Need to show loading/error/success states |\n| `pipe` | Chaining multiple transformations |\n\n---\n\n## 1. State with Option (Maybe It's There, Maybe Not)\n\nUse `Option` instead of `null | undefined` for clearer intent.\n\n### Basic Pattern\n\n```typescript\nimport { useState } from 'react'\nimport * as O from 'fp-ts/Option'\nimport { pipe } from 'fp-ts/function'\n\ninterface User {\n  id: string\n  name: string\n  email: string\n}\n\nfunction UserProfile() {\n  // Option says \"this might not exist yet\"\n  const [user, setUser] = useState<O.Option<User>>(O.none)\n\n  const handleLogin = (userData: User) => {\n    setUser(O.some(userData))\n  }\n\n  const handleLogout = () => {\n    setUser(O.none)\n  }\n\n  return pipe(\n    user,\n    O.match(\n      // When there's no user\n      () => <button onClick={() => handleLogin({ id: '1', name: 'Alice', email: 'alice@example.com' })}>\n        Log In\n      </button>,\n      // When there's a user\n      (u) => (\n        <div>\n          <p>Welcome, {u.name}!</p>\n          <button onClick={handleLogout}>Log Out</button>\n        </div>\n      )\n    )\n  )\n}\n```\n\n### Chaining Optional Values\n\n```typescript\nimport * as O from 'fp-ts/Option'\nimport { pipe } from 'fp-ts/function'\n\ninterface Profile {\n  user: O.Option<{\n    name: string\n    settings: O.Option<{\n      theme: string\n    }>\n  }>\n}\n\nfunction getTheme(profile: Profile): string {\n  return pipe(\n    profile.user,\n    O.flatMap(u => u.settings),\n    O.map(s => s.theme),\n    O.getOrElse(() => 'light') // default\n  )\n}\n```\n\n---\n\n## 2. Form Validation with Either\n\nEither is perfect for validation: `Left` = errors, `Right` = valid data.\n\n### Simple Form Validation\n\n```typescript\nimport * as E from 'fp-ts/Either'\nimport * as A from 'fp-ts/Array'\nimport { pipe } from 'fp-ts/function'\n\n// Validation functions return Either<ErrorMessage, ValidValue>\nconst validateEmail = (email: string): E.Either<string, string> =>\n  email.includes('@')\n    ? E.right(email)\n    : E.left('Invalid email address')\n\nconst validatePassword = (password: string): E.Either<string, string> =>\n  password.length >= 8\n    ? E.right(password)\n    : E.left('Password must be at least 8 characters')\n\nconst validateName = (name: string): E.Either<string, string> =>\n  name.trim().length > 0\n    ? E.right(name.trim())\n    : E.left('Name is required')\n```\n\n### Collecting All Errors (Not Just First One)\n\n```typescript\nimport * as E from 'fp-ts/Either'\nimport { sequenceS } from 'fp-ts/Apply'\nimport { getSemigroup } from 'fp-ts/NonEmptyArray'\nimport { pipe } from 'fp-ts/function'\n\n// This collects ALL errors, not just the first one\nconst validateAll = sequenceS(E.getApplicativeValidation(getSemigroup<string>()))\n\ninterface SignupForm {\n  name: string\n  email: string\n  password: string\n}\n\ninterface ValidatedForm {\n  name: string\n  email: string\n  password: string\n}\n\nfunction validateForm(form: SignupForm): E.Either<string[], ValidatedForm> {\n  return pipe(\n    validateAll({\n      name: pipe(validateName(form.name), E.mapLeft(e => [e])),\n      email: pipe(validateEmail(form.email), E.mapLeft(e => [e])),\n      password: pipe(validatePassword(form.password), E.mapLeft(e => [e])),\n    })\n  )\n}\n\n// Usage in component\nfunction SignupForm() {\n  const [form, setForm] = useState({ name: '', email: '', password: '' })\n  const [errors, setErrors] = useState<string[]>([])\n\n  const handleSubmit = () => {\n    pipe(\n      validateForm(form),\n      E.match(\n        (errs) => setErrors(errs),     // Show all errors\n        (valid) => {\n          setErrors([])\n          submitToServer(valid)         // Submit valid data\n        }\n      )\n    )\n  }\n\n  return (\n    <form onSubmit={e => { e.preventDefault(); handleSubmit() }}>\n      <input\n        value={form.name}\n        onChange={e => setForm(f => ({ ...f, name: e.target.value }))}\n        placeholder=\"Name\"\n      />\n      <input\n        value={form.email}\n        onChange={e => setForm(f => ({ ...f, email: e.target.value }))}\n        placeholder=\"Email\"\n      />\n      <input\n        type=\"password\"\n        value={form.password}\n        onChange={e => setForm(f => ({ ...f, password: e.target.value }))}\n        placeholder=\"Password\"\n      />\n\n      {errors.length > 0 && (\n        <ul style={{ color: 'red' }}>\n          {errors.map((err, i) => <li key={i}>{err}</li>)}\n        </ul>\n      )}\n\n      <button type=\"submit\">Sign Up</button>\n    </form>\n  )\n}\n```\n\n### Field-Level Errors (Better UX)\n\n```typescript\ntype FieldErrors = Partial<Record<keyof SignupForm, string>>\n\nfunction validateFormWithFieldErrors(form: SignupForm): E.Either<FieldErrors, ValidatedForm> {\n  const errors: FieldErrors = {}\n\n  pipe(validateName(form.name), E.mapLeft(e => { errors.name = e }))\n  pipe(validateEmail(form.email), E.mapLeft(e => { errors.email = e }))\n  pipe(validatePassword(form.password), E.mapLeft(e => { errors.password = e }))\n\n  return Object.keys(errors).length > 0\n    ? E.left(errors)\n    : E.right({ name: form.name.trim(), email: form.email, password: form.password })\n}\n\n// In component\n{errors.email && <span className=\"error\">{errors.email}</span>}\n```\n\n---\n\n## 3. Data Fetching with TaskEither\n\nTaskEither = async operation that might fail. Perfect for API calls.\n\n### Basic Fetch Hook\n\n```typescript\nimport { useState, useEffect } from 'react'\nimport * as TE from 'fp-ts/TaskEither'\nimport * as E from 'fp-ts/Either'\nimport { pipe } from 'fp-ts/function'\n\n// Wrap fetch in TaskEither\nconst fetchJson = <T>(url: string): TE.TaskEither<Error, T> =>\n  TE.tryCatch(\n    async () => {\n      const res = await fetch(url)\n      if (!res.ok) throw new Error(`HTTP ${res.status}`)\n      return res.json()\n    },\n    (err) => err instanceof Error ? err : new Error(String(err))\n  )\n\n// Custom hook\nfunction useFetch<T>(url: string) {\n  const [data, setData] = useState<T | null>(null)\n  const [error, setError] = useState<Error | null>(null)\n  const [loading, setLoading] = useState(true)\n\n  useEffect(() => {\n    setLoading(true)\n    setError(null)\n\n    pipe(\n      fetchJson<T>(url),\n      TE.match(\n        (err) => {\n          setError(err)\n          setLoading(false)\n        },\n        (result) => {\n          setData(result)\n          setLoading(false)\n        }\n      )\n    )()\n  }, [url])\n\n  return { data, error, loading }\n}\n\n// Usage\nfunction UserList() {\n  const { data, error, loading } = useFetch<User[]>('/api/users')\n\n  if (loading) return <div>Loading...</div>\n  if (error) return <div>Error: {error.message}</div>\n  return (\n    <ul>\n      {data?.map(user => <li key={user.id}>{user.name}</li>)}\n    </ul>\n  )\n}\n```\n\n### Chaining API Calls\n\n```typescript\n// Fetch user, then fetch their posts\nconst fetchUserWithPosts = (userId: string) => pipe(\n  fetchJson<User>(`/api/users/${userId}`),\n  TE.flatMap(user => pipe(\n    fetchJson<Post[]>(`/api/users/${userId}/posts`),\n    TE.map(posts => ({ ...user, posts }))\n  ))\n)\n```\n\n### Parallel API Calls\n\n```typescript\nimport { sequenceT } from 'fp-ts/Apply'\n\n// Fetch multiple things at once\nconst fetchDashboardData = () => pipe(\n  sequenceT(TE.ApplyPar)(\n    fetchJson<User>('/api/user'),\n    fetchJson<Stats>('/api/stats'),\n    fetchJson<Notifications[]>('/api/notifications')\n  ),\n  TE.map(([user, stats, notifications]) => ({\n    user,\n    stats,\n    notifications\n  }))\n)\n```\n\n---\n\n## 4. RemoteData Pattern (The Right Way to Handle Async State)\n\nStop using `{ data, loading, error }` booleans. Use a proper state machine.\n\n### The Pattern\n\n```typescript\n// RemoteData has exactly 4 states - no impossible combinations\ntype RemoteData<E, A> =\n  | { _tag: 'NotAsked' }                    // Haven't started yet\n  | { _tag: 'Loading' }                     // In progress\n  | { _tag: 'Failure'; error: E }           // Failed\n  | { _tag: 'Success'; data: A }            // Got it!\n\n// Constructors\nconst notAsked = <E, A>(): RemoteData<E, A> => ({ _tag: 'NotAsked' })\nconst loading = <E, A>(): RemoteData<E, A> => ({ _tag: 'Loading' })\nconst failure = <E, A>(error: E): RemoteData<E, A> => ({ _tag: 'Failure', error })\nconst success = <E, A>(data: A): RemoteData<E, A> => ({ _tag: 'Success', data })\n\n// Pattern match all states\nfunction fold<E, A, R>(\n  rd: RemoteData<E, A>,\n  onNotAsked: () => R,\n  onLoading: () => R,\n  onFailure: (e: E) => R,\n  onSuccess: (a: A) => R\n): R {\n  switch (rd._tag) {\n    case 'NotAsked': return onNotAsked()\n    case 'Loading': return onLoading()\n    case 'Failure': return onFailure(rd.error)\n    case 'Success': return onSuccess(rd.data)\n  }\n}\n```\n\n### Hook with RemoteData\n\n```typescript\nfunction useRemoteData<T>(fetchFn: () => Promise<T>) {\n  const [state, setState] = useState<RemoteData<Error, T>>(notAsked())\n\n  const execute = async () => {\n    setState(loading())\n    try {\n      const data = await fetchFn()\n      setState(success(data))\n    } catch (err) {\n      setState(failure(err instanceof Error ? err : new Error(String(err))))\n    }\n  }\n\n  return { state, execute }\n}\n\n// Usage\nfunction UserProfile({ userId }: { userId: string }) {\n  const { state, execute } = useRemoteData(() =>\n    fetch(`/api/users/${userId}`).then(r => r.json())\n  )\n\n  useEffect(() => { execute() }, [userId])\n\n  return fold(\n    state,\n    () => <button onClick={execute}>Load User</button>,\n    () => <Spinner />,\n    (err) => <ErrorMessage message={err.message} onRetry={execute} />,\n    (user) => <UserCard user={user} />\n  )\n}\n```\n\n### Why RemoteData Beats Booleans\n\n```typescript\n// ❌ BAD: Impossible states are possible\ninterface BadState {\n  data: User | null\n  loading: boolean\n  error: Error | null\n}\n// Can have: { data: user, loading: true, error: someError } - what does that mean?!\n\n// ✅ GOOD: Only valid states exist\ntype GoodState = RemoteData<Error, User>\n// Can only be: NotAsked | Loading | Failure | Success\n```\n\n---\n\n## 5. Referential Stability (Preventing Re-renders)\n\nfp-ts values like `O.some(1)` create new objects each render. React sees them as \"changed\".\n\n### The Problem\n\n```typescript\n// ❌ BAD: Creates new Option every render\nfunction BadComponent() {\n  const [value, setValue] = useState(O.some(1))\n\n  useEffect(() => {\n    // This runs EVERY render because O.some(1) !== O.some(1)\n    console.log('value changed')\n  }, [value])\n}\n```\n\n### Solution 1: useMemo\n\n```typescript\n// ✅ GOOD: Memoize Option creation\nfunction GoodComponent() {\n  const [rawValue, setRawValue] = useState<number | null>(1)\n\n  const value = useMemo(\n    () => O.fromNullable(rawValue),\n    [rawValue]  // Only recreate when rawValue changes\n  )\n\n  useEffect(() => {\n    // Now this only runs when rawValue actually changes\n    console.log('value changed')\n  }, [rawValue])  // Depend on raw value, not Option\n}\n```\n\n### Solution 2: fp-ts-react-stable-hooks\n\n```bash\nnpm install fp-ts-react-stable-hooks\n```\n\n```typescript\nimport { useStableO, useStableEffect } from 'fp-ts-react-stable-hooks'\nimport * as O from 'fp-ts/Option'\nimport * as Eq from 'fp-ts/Eq'\n\nfunction StableComponent() {\n  // Uses fp-ts equality instead of reference equality\n  const [value, setValue] = useStableO(O.some(1))\n\n  // Effect that understands Option equality\n  useStableEffect(\n    () => { console.log('value changed') },\n    [value],\n    Eq.tuple(O.getEq(Eq.eqNumber))  // Custom equality\n  )\n}\n```\n\n---\n\n## 6. Dependency Injection with Context\n\nUse ReaderTaskEither for testable components with injected dependencies.\n\n### Setup Dependencies\n\n```typescript\nimport * as RTE from 'fp-ts/ReaderTaskEither'\nimport { pipe } from 'fp-ts/function'\nimport { createContext, useContext, ReactNode } from 'react'\n\n// Define what services your app needs\ninterface AppDependencies {\n  api: {\n    getUser: (id: string) => Promise<User>\n    updateUser: (id: string, data: Partial<User>) => Promise<User>\n  }\n  analytics: {\n    track: (event: string, data?: object) => void\n  }\n}\n\n// Create context\nconst DepsContext = createContext<AppDependencies | null>(null)\n\n// Provider\nfunction AppProvider({ deps, children }: { deps: AppDependencies; children: ReactNode }) {\n  return <DepsContext.Provider value={deps}>{children}</DepsContext.Provider>\n}\n\n// Hook to use dependencies\nfunction useDeps(): AppDependencies {\n  const deps = useContext(DepsContext)\n  if (!deps) throw new Error('Missing AppProvider')\n  return deps\n}\n```\n\n### Use in Components\n\n```typescript\nfunction UserProfile({ userId }: { userId: string }) {\n  const { api, analytics } = useDeps()\n  const [user, setUser] = useState<RemoteData<Error, User>>(notAsked())\n\n  useEffect(() => {\n    setUser(loading())\n    api.getUser(userId)\n      .then(u => {\n        setUser(success(u))\n        analytics.track('user_viewed', { userId })\n      })\n      .catch(e => setUser(failure(e)))\n  }, [userId, api, analytics])\n\n  // render...\n}\n```\n\n### Testing with Mock Dependencies\n\n```typescript\nconst mockDeps: AppDependencies = {\n  api: {\n    getUser: jest.fn().mockResolvedValue({ id: '1', name: 'Test User' }),\n    updateUser: jest.fn().mockResolvedValue({ id: '1', name: 'Updated' }),\n  },\n  analytics: {\n    track: jest.fn(),\n  },\n}\n\ntest('loads user on mount', async () => {\n  render(\n    <AppProvider deps={mockDeps}>\n      <UserProfile userId=\"1\" />\n    </AppProvider>\n  )\n\n  await screen.findByText('Test User')\n  expect(mockDeps.api.getUser).toHaveBeenCalledWith('1')\n})\n```\n\n---\n\n## 7. React 19 Patterns\n\n### use() for Promises (React 19+)\n\n```typescript\nimport { use, Suspense } from 'react'\n\n// Instead of useEffect + useState for data fetching\nfunction UserProfile({ userPromise }: { userPromise: Promise<User> }) {\n  const user = use(userPromise)  // Suspends until resolved\n  return <div>{user.name}</div>\n}\n\n// Parent provides the promise\nfunction App() {\n  const userPromise = fetchUser('1')  // Start fetching immediately\n\n  return (\n    <Suspense fallback={<Spinner />}>\n      <UserProfile userPromise={userPromise} />\n    </Suspense>\n  )\n}\n```\n\n### useActionState for Forms (React 19+)\n\n```typescript\nimport { useActionState } from 'react'\nimport * as E from 'fp-ts/Either'\n\ninterface FormState {\n  errors: string[]\n  success: boolean\n}\n\nasync function submitForm(\n  prevState: FormState,\n  formData: FormData\n): Promise<FormState> {\n  const data = {\n    email: formData.get('email') as string,\n    password: formData.get('password') as string,\n  }\n\n  // Use Either for validation\n  const result = pipe(\n    validateForm(data),\n    E.match(\n      (errors) => ({ errors, success: false }),\n      async (valid) => {\n        await saveToServer(valid)\n        return { errors: [], success: true }\n      }\n    )\n  )\n\n  return result\n}\n\nfunction SignupForm() {\n  const [state, formAction, isPending] = useActionState(submitForm, {\n    errors: [],\n    success: false\n  })\n\n  return (\n    <form action={formAction}>\n      <input name=\"email\" type=\"email\" />\n      <input name=\"password\" type=\"password\" />\n\n      {state.errors.map(e => <p key={e} className=\"error\">{e}</p>)}\n\n      <button disabled={isPending}>\n        {isPending ? 'Submitting...' : 'Sign Up'}\n      </button>\n    </form>\n  )\n}\n```\n\n### useOptimistic for Instant Feedback (React 19+)\n\n```typescript\nimport { useOptimistic } from 'react'\n\nfunction TodoList({ todos }: { todos: Todo[] }) {\n  const [optimisticTodos, addOptimisticTodo] = useOptimistic(\n    todos,\n    (state, newTodo: Todo) => [...state, { ...newTodo, pending: true }]\n  )\n\n  const addTodo = async (text: string) => {\n    const newTodo = { id: crypto.randomUUID(), text, done: false }\n\n    // Immediately show in UI\n    addOptimisticTodo(newTodo)\n\n    // Actually save (will reconcile when done)\n    await saveTodo(newTodo)\n  }\n\n  return (\n    <ul>\n      {optimisticTodos.map(todo => (\n        <li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>\n          {todo.text}\n        </li>\n      ))}\n    </ul>\n  )\n}\n```\n\n---\n\n## 8. Common Patterns Cheat Sheet\n\n### Render Based on Option\n\n```typescript\n// Pattern 1: match\npipe(\n  maybeUser,\n  O.match(\n    () => <LoginButton />,\n    (user) => <UserMenu user={user} />\n  )\n)\n\n// Pattern 2: fold (same as match)\nO.fold(\n  () => <LoginButton />,\n  (user) => <UserMenu user={user} />\n)(maybeUser)\n\n// Pattern 3: getOrElse for simple defaults\nconst name = pipe(\n  maybeUser,\n  O.map(u => u.name),\n  O.getOrElse(() => 'Guest')\n)\n```\n\n### Render Based on Either\n\n```typescript\npipe(\n  validationResult,\n  E.match(\n    (errors) => <ErrorList errors={errors} />,\n    (data) => <SuccessMessage data={data} />\n  )\n)\n```\n\n### Safe Array Rendering\n\n```typescript\nimport * as A from 'fp-ts/Array'\n\n// Get first item safely\nconst firstUser = pipe(\n  users,\n  A.head,\n  O.map(user => <Featured user={user} />),\n  O.getOrElse(() => <NoFeaturedUser />)\n)\n\n// Find specific item\nconst adminUser = pipe(\n  users,\n  A.findFirst(u => u.role === 'admin'),\n  O.map(admin => <AdminBadge user={admin} />),\n  O.toNullable  // or O.getOrElse(() => null)\n)\n```\n\n### Conditional Props\n\n```typescript\n// Add props only if value exists\nconst modalProps = {\n  isOpen: true,\n  ...pipe(\n    maybeTitle,\n    O.map(title => ({ title })),\n    O.getOrElse(() => ({}))\n  )\n}\n```\n\n---\n\n## When to Use What\n\n| Situation | Use |\n|-----------|-----|\n| Value might not exist | `Option<T>` |\n| Operation might fail (sync) | `Either<E, A>` |\n| Async operation might fail | `TaskEither<E, A>` |\n| Need loading/error/success UI | `RemoteData<E, A>` |\n| Form with multiple validations | `Either` with validation applicative |\n| Dependency injection | Context + `ReaderTaskEither` |\n| Prevent re-renders with fp-ts | `useMemo` or `fp-ts-react-stable-hooks` |\n\n---\n\n## Libraries\n\n- **[fp-ts](https://github.com/gcanti/fp-ts)** - Core library\n- **[fp-ts-react-stable-hooks](https://github.com/mblink/fp-ts-react-stable-hooks)** - Stable hooks\n- **[@devexperts/remote-data-ts](https://github.com/devexperts/remote-data-ts)** - RemoteData\n- **[io-ts](https://github.com/gcanti/io-ts)** - Runtime type validation\n- **[zod](https://github.com/colinhacks/zod)** - Schema validation (works great with fp-ts)\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.","tags":["react","antigravity","awesome","skills","sickn33","agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding","ai-workflows","antigravity-skills"],"capabilities":["skill","source-sickn33","skill-fp-ts-react","topic-agent-skills","topic-agentic-skills","topic-ai-agent-skills","topic-ai-agents","topic-ai-coding","topic-ai-workflows","topic-antigravity","topic-antigravity-skills","topic-claude-code","topic-claude-code-skills","topic-codex-cli","topic-codex-skills"],"categories":["antigravity-awesome-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/sickn33/antigravity-awesome-skills/fp-ts-react","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add sickn33/antigravity-awesome-skills","source_repo":"https://github.com/sickn33/antigravity-awesome-skills","install_from":"skills.sh"}},"qualityScore":"0.700","qualityRationale":"deterministic score 0.70 from registry signals: · indexed on github topic:agent-skills · 34793 github stars · SKILL.md body (18,845 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-04-24T00:50:58.826Z","embedding":null,"createdAt":"2026-04-18T21:37:32.758Z","updatedAt":"2026-04-24T00:50:58.826Z","lastSeenAt":"2026-04-24T00:50:58.826Z","tsv":"'/api/notifications':852 '/api/stats':849 '/api/user':847 '/api/users':777,811,818,1061 '/apply':402,835 '/array':317,1803 '/colinhacks/zod)**':1954 '/devexperts/remote-data-ts)**':1940 '/either':309,395,675,1580 '/eq':1281 '/function':169,255,324,416,682,1344 '/gcanti/fp-ts)**':1923 '/gcanti/io-ts)**':1947 '/mblink/fp-ts-react-stable-hooks)**':1934 '/nonemptyarray':409 '/option':162,248,1273 '/posts':820 '/readertaskeither':1337 '/taskeither':667 '0':373,559,622 '0.5':1726 '1':129,217,1149,1176,1184,1186,1192,1207,1298,1476,1484,1507,1553,1727,1740 '14/15':33,88 '18/19':31,85 '19':1510,1516,1567,1667 '2':283,1239,1750 '3':636,1762 '4':860,887 '5':1136 '6':1314 '7':1508 '8':353,362,1729 'a.findfirst':1826 'a.head':1812 'accumul':81 'action':1645 'actual':1226,1708 'add':1842 'addoptimistictodo':1680,1706 'address':344 'addtodo':1691 'admin':1829,1831,1834 'adminbadg':1832 'adminus':1823 'alic':219 'alice@example.com':221 'analyt':1370,1430,1461,1487 'analytics.track':1450 'api':117,649,796,826,1359,1429,1460,1471 'api.getuser':1443 'app':23,42,57,1355,1549 'appdepend':1358,1382,1391,1405,1470 'applic':1896 'appprovid':1387,1416,1497 'array':1793 'ask':1996 'async':113,642,695,868,1024,1495,1587,1621,1692,1876 'await':698,1030,1500,1623,1714 'bad':1092,1163 'badcompon':1170 'badstat':1098 'base':1735,1777 'bash':1246 'basic':148,651 'beat':1089 'better':577 'boolean':875,1090,1103,1586 'boundari':2004 'build':21,55 'button':213,232,1072,1655 'call':118,650,797,827 'case':988,992,996,1001 'catch':1035,1454 'chain':126,237,795 'chang':1159,1189,1218,1227,1230,1307 'charact':363 'cheat':1732 'children':1389,1392,1398 'clarif':1998 'classnam':1652 'clear':1971 'clearer':146 'code':46 'collect':380,418 'color':562 'combin':891 'common':1730 'compon':480,633,1323,1421 'condit':1839 'console.log':1187,1228,1305 'const':187,193,200,331,345,364,426,483,490,495,594,687,696,725,732,739,771,805,841,918,927,936,948,1014,1022,1028,1056,1171,1201,1208,1293,1379,1406,1428,1432,1468,1535,1550,1595,1611,1634,1678,1690,1695,1767,1808,1822,1848 'constructor':917 'context':1318,1378,1899 'core':1924 'creat':1150,1164,1377 'createcontext':1346,1381 'creation':1198 'criteria':2007 'crypto.randomuuid':1698 'custom':719,1312 'data':17,73,297,513,637,726,765,772,788,872,913,952,959,1029,1034,1099,1109,1367,1374,1528,1596,1615,1788,1790,1791 'default':282,1766 'defin':1351 'dep':1388,1390,1397,1407,1411,1418,1498 'depend':1232,1315,1326,1328,1402,1466,1897 'depscontext':1380,1409 'depscontext.provider':1395 'describ':1975 'devexperts/remote-data-ts':1937 'disabl':1656 'done':1700,1713 'e':304,390,462,463,469,470,476,477,517,524,536,550,601,603,608,610,615,617,670,894,909,920,923,929,932,938,941,943,950,955,966,971,978,979,1455,1458,1575,1648,1651,1654,1874,1881,1887 'e.either':335,349,368,451,591 'e.getapplicativevalidation':429 'e.left':341,356,376,623 'e.mapleft':461,468,475,600,607,614 'e.match':500,1616,1783 'e.preventdefault':518 'e.right':339,354,374,625 'e.target.value':529,541,555 'effect':1299 'either':106,287,288,328,1608,1779,1873,1893 'email':176,220,333,340,343,435,443,464,488,540,543,628,1597,1599 'email.includes':338 'environ':1987 'environment-specif':1986 'eq':1276 'eq.eqnumber':1311 'eq.tuple':1309 'equal':1288,1292,1303,1313 'err':501,503,565,570,710,711,714,718,753,755,1036,1039,1042,1046,1077 'err.message':1080 'error':80,294,382,420,491,506,576,595,620,624,692,705,713,716,733,736,766,773,783,785,874,908,940,947,1019,1041,1044,1104,1105,1113,1127,1414,1437,1583,1617,1618,1627,1640,1653,1784,1786,1787 'error.message':786 'errorlist':1785 'errormessag':329,1078 'errors.email':609,634,635 'errors.length':558 'errors.map':564 'errors.name':602 'errors.password':616 'event':1372 'everi':1167,1180 'exact':886 'execut':1023,1049,1058,1067,1074,1082 'exist':185,1123,1847,1867 'expect':1504 'expert':1992 'f':526,527,538,539,552,553 'fail':109,116,646,910,1871,1879 'failur':907,937,946,997,1038,1134,1457 'fallback':1559 'fals':757,762,1620,1642,1701 'featur':1815 'feedback':1665 'fetch':18,74,638,652,684,699,799,802,836,1060,1529,1555 'fetchdashboarddata':842 'fetchfn':1012,1031 'fetchjson':688,750,810,816,846,848,850 'fetchus':1552 'fetchuserwithpost':806 'field':574 'field-level':573 'fielderror':581,592,596 'find':1819 'first':385,424,1805 'firstus':1809 'fold':965,1070,1751 'form':16,77,110,284,299,449,484,499,515,589,1565,1644,1889 'form.email':467,534,606,629 'form.name':460,522,599 'form.name.trim':627 'form.password':474,548,613,631 'formact':1636,1646 'formdata':1592,1593 'formdata.get':1598,1603 'formstat':1582,1591 'fp':2,10,60,160,167,246,253,307,315,322,393,400,407,414,665,673,680,833,1144,1241,1250,1261,1271,1279,1286,1335,1342,1578,1801,1907,1912,1919,1927,1961 'fp-ts':9,59,159,166,245,252,306,314,321,392,399,406,413,664,672,679,832,1143,1270,1278,1285,1334,1341,1577,1800,1906,1918,1960 'fp-ts-react':1 'fp-ts-react-stable-hook':1240,1249,1260,1911,1926 'function':25,34,90,178,266,326,447,481,587,721,769,964,1010,1051,1169,1199,1282,1386,1403,1423,1530,1548,1588,1632,1673 'get':1804 'getorels':1763 'getsemigroup':404,430 'getthem':267 'getus':1360,1472 'github.com':1922,1933,1939,1946,1953 'github.com/colinhacks/zod)**':1952 'github.com/devexperts/remote-data-ts)**':1938 'github.com/gcanti/fp-ts)**':1921 'github.com/gcanti/io-ts)**':1945 'github.com/mblink/fp-ts-react-stable-hooks)**':1932 'good':1119,1195 'goodcompon':1200 'goodstat':1125 'got':915 'great':1958 'guest':1775 'handl':69,867 'handlelogin':194,215 'handlelogout':201,234 'handlesubmit':496,519 'haven':898 'hook':14,653,720,1006,1245,1254,1265,1399,1916,1931,1936 'http':706 'id':172,216,1361,1365,1475,1483,1697 'immedi':1556,1702 'implement':76 'import':151,155,163,241,249,302,310,318,388,396,403,410,655,660,668,676,829,1256,1266,1274,1330,1338,1345,1518,1569,1573,1669,1796 'imposs':890,1093 'inject':1316,1325,1898 'input':520,532,544,2001 'instal':1248 'instanceof':712,1040 'instant':1664 'instead':141,1289,1523 'intent':147 'interfac':170,256,431,439,1097,1357,1581 'invalid':342 'io':1943 'io-t':1942 'isopen':1850 'ispend':1637,1657,1658 'item':1806,1821 'jargon':44 'jest.fn':1473,1481,1489 'key':568,792,1650,1721 'keyof':584 'least':361 'left':293 'length':372,621 'level':575 'li':567,791,1720 'librari':1917,1925 'light':281 'like':1147 'limit':1963 'load':104,740,767,774,779,781,873,903,928,935,993,1026,1075,1102,1111,1133,1442,1491 'loading/error/success':70,123,1884 'log':222,235 'machin':880 'manag':67 'map':789 'match':961,1741,1754,1972 'mayb':133,137 'maybetitl':1853 'maybeus':1743,1760,1770 'mean':1118 'memoiz':1196 'messag':1079 'might':99,108,115,183,645,1865,1870,1878 'miss':101,1415,2009 'mock':1465 'mockdep':1469,1499 'mockdeps.api.getuser':1505 'mockresolvedvalu':1474,1482 'modalprop':1849 'mount':1494 'multipl':127,837,1891 'must':358 'name':174,218,260,366,377,433,441,457,487,528,531,626,1477,1485,1768 'name.trim':371,375 'need':120,1356,1883 'new':704,715,1043,1151,1165,1413 'newtodo':1684,1687,1696,1707,1716 'next.js':32,87 'notask':897,919,926,989,1021,1132,1439 'notif':851,856,859 'npm':1247 'null':143,730,731,737,738,748,1101,1106,1206,1383,1384,1838 'number':1205 'o':157,243,1268 'o.flatmap':274 'o.fold':1755 'o.fromnullable':1211 'o.geteq':1310 'o.getorelse':280,1774,1818,1837,1857 'o.map':277,1771,1813,1830,1854 'o.match':207,1744 'o.none':192,203 'o.option':191,259,263 'o.some':198,1148,1175,1183,1185,1297 'o.tonullable':1835 'object':1152,1375 'object.keys':619 'onchang':523,535,549 'onclick':214,233,1073 'one':386,425 'onfailur':977,999 'onload':975,995 'onnotask':973,991 'onretri':1081 'onsubmit':516 'onsuccess':981,1004 'opac':1724 'oper':107,114,643,1869,1877 'optimistictodo':1679 'optimistictodos.map':1718 'option':97,132,140,180,238,1166,1197,1237,1302,1737,1868 'output':1981 'p':1649 'parallel':825 'parent':1544 'partial':582,1368 'password':347,355,357,437,445,471,489,546,554,557,630,1602,1604 'password.length':352 'pattern':6,27,39,91,94,149,862,882,960,1511,1731,1739,1749,1761 'pend':1688 'perfect':290,647 'permiss':2002 'pipe':125,164,205,250,272,319,411,455,458,465,472,497,597,604,611,677,749,809,815,843,1339,1613,1742,1769,1781,1810,1824,1852 'placehold':530,542,556 'possibl':1096 'post':804,817,822,824 'practic':5,38 'prevent':1139,1901 'prevstat':1590 'problem':1161 'profil':257,268,269 'profile.user':273 'program':26,35 'progress':905 'promis':1013,1363,1369,1514,1534,1547,1594 'prop':1840,1843 'proper':878 'provid':1385,1545 'quick':92 'r':968,974,976,980,984,985,1064 'r.json':1065 'raw':1234 'rawvalu':1202,1212,1213,1217,1225,1231 'rd':969 'rd._tag':987 'rd.data':1005 'rd.error':1000 're':1141,1903 're-rend':1140,1902 'react':4,13,22,30,37,41,56,84,154,659,1155,1243,1252,1263,1350,1509,1515,1522,1566,1572,1666,1672,1914,1929 'reactnod':1348,1393 'readertaskeith':1320,1900 'reconcil':1711 'record':583 'recreat':1215 'red':563 'refer':93,1291 'referenti':1137 'remotedata':119,861,884,893,922,931,942,954,970,1008,1018,1088,1126,1436,1886,1941 'render':1142,1154,1168,1181,1462,1496,1734,1776,1794,1904 'requir':379,2000 'res':697 'res.json':709 'res.ok':702 'res.status':707 'resolv':1541 'result':758,760,1612,1631 'return':204,271,327,454,514,618,708,764,780,784,787,990,994,998,1003,1047,1069,1394,1417,1542,1557,1626,1630,1643,1717 'review':1993 'right':295,864 'rte':1332 'run':1179,1223 'runtim':1948 's.theme':279 'safe':65,1792,1807 'safeti':2003 'save':1709 'savetodo':1715 'savetoserv':1624 'say':181 'schema':1955 'scope':1974 'screen.findbytext':1501 'see':1156 'sequenc':397,428 'sequencet':830,844 'servic':1353 'set':262 'setdata':727,759 'seterror':492,502,508,734,747,754 'setform':485,525,537,551 'setload':741,745,756,761 'setrawvalu':1203 'setstat':1016,1025,1032,1037 'setup':1327 'setus':189,197,202,1434,1441,1447,1456 'setvalu':1173,1295 'sheet':1733 'show':122,504,1703 'sign':571,1660 'signupform':432,450,482,585,590,1633 'simpl':298,1765 'situat':1862 'skill':53,1966 'skill-fp-ts-react' 'solut':1191,1238 'someerror':1114 'source-sickn33' 'specif':1820,1988 'stabil':1138 'stabl':1244,1253,1264,1915,1930,1935 'stablecompon':1283 'start':900,1554 'stat':855,858 'state':15,66,71,124,130,869,879,888,963,1015,1048,1057,1071,1094,1122,1635,1683,1686 'state.errors.map':1647 'stop':870,1994 'string':173,175,177,261,265,270,334,336,337,348,350,351,367,369,370,434,436,438,442,444,446,452,494,586,690,717,724,808,1045,1055,1362,1366,1373,1427,1584,1601,1606,1694 'style':561,1723 'submit':511,1659 'submitform':1589,1639 'submittoserv':509 'substitut':1984 'success':912,949,958,1002,1033,1135,1448,1585,1619,1628,1641,2006 'successmessag':1789 'suspend':1539 'suspens':1520,1558 'switch':986 'sync':1872 'tag':896,902,906,911,925,934,945,957 'task':1970 'taskeith':112,640,641,686,1880 'te':662 'te.applypar':845 'te.flatmap':813 'te.map':821,853 'te.match':752 'te.taskeither':691 'te.trycatch':694 'test':1463,1478,1490,1502,1990 'testabl':1322 'text':1693,1699 'theme':264 'thing':838 'throw':703,1412 'titl':1855,1856 'todo':1675,1676,1677,1682,1685,1719 'todo.id':1722 'todo.pending':1725 'todo.text':1728 'todolist':1674 'tohavebeencalledwith':1506 'topic-agent-skills' 'topic-agentic-skills' 'topic-ai-agent-skills' 'topic-ai-agents' 'topic-ai-coding' 'topic-ai-workflows' 'topic-antigravity' 'topic-antigravity-skills' 'topic-claude-code' 'topic-claude-code-skills' 'topic-codex-cli' 'topic-codex-skills' 'track':1371,1488 'transform':128 'treat':1979 'tri':1027 'true':743,746,1112,1629,1689,1851 'ts':3,11,61,161,168,247,254,308,316,323,394,401,408,415,666,674,681,834,1145,1242,1251,1262,1272,1280,1287,1336,1343,1579,1802,1908,1913,1920,1928,1944,1962 'type':64,545,580,892,1124,1949 'type-saf':63 'typescript':150,240,301,387,579,654,798,828,883,1009,1091,1162,1194,1255,1329,1422,1467,1517,1568,1668,1738,1780,1795,1841 'u':229,275,1446,1449,1772,1827 'u.name':231,1773 'u.role':1828 'u.settings':276 'ui':1705,1885 'ul':560 'undefin':144 'understand':1301 'updat':1486 'updateus':1364,1480 'url':689,700,723,751,763 'usag':478,768,1050 'use':8,19,51,83,95,139,871,876,1284,1319,1401,1419,1512,1519,1537,1607,1860,1863,1964 'useactionst':1563,1570,1638 'usecontext':1347,1408 'usedep':1404,1431 'useeffect':657,744,1066,1177,1219,1440,1525 'usefetch':722,775 'usememo':1193,1210,1909 'useoptimist':1662,1670,1681 'user':102,171,188,196,206,212,228,258,776,790,800,814,823,854,857,1076,1083,1085,1086,1100,1110,1128,1433,1438,1451,1479,1492,1503,1536,1745,1747,1748,1756,1758,1759,1811,1814,1816,1817,1825,1833 'user.id':793 'user.name':794,1543 'usercard':1084 'userdata':195,199 'useremotedata':1011,1059 'userid':807,812,819,1053,1054,1062,1068,1425,1426,1444,1453,1459 'userlist':770 'usermenu':1746,1757 'userprofil':179,1052,1424,1531,1560 'userpromis':1532,1533,1538,1551,1561,1562 'usest':152,190,486,493,656,728,735,742,1017,1174,1204,1435,1526 'usestableeffect':1258,1304 'usestableo':1257,1296 'ux':578 'valid':78,111,285,292,296,300,325,507,510,512,1121,1610,1622,1625,1892,1895,1950,1956,1989 'validateal':427,456 'validatedform':440,453,593 'validateemail':332,466,605 'validateform':448,498,1614 'validateformwithfielderror':588 'validatenam':365,459,598 'validatepassword':346,473,612 'validationresult':1782 'validvalu':330 'valu':98,239,521,533,547,1146,1172,1188,1190,1209,1229,1235,1294,1306,1308,1396,1846,1864 'view':1452 'void':1376 'way':865 'welcom':230 'work':28,48,1957 'wrap':683 'yet':105,186,901 'zod':1951","prices":[{"id":"b604e40a-1a48-4954-811a-e515be3957eb","listingId":"fb02597c-7d1f-4d0b-9b02-f65c92e91d3e","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"sickn33","category":"antigravity-awesome-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T21:37:32.758Z"}],"sources":[{"listingId":"fb02597c-7d1f-4d0b-9b02-f65c92e91d3e","source":"github","sourceId":"sickn33/antigravity-awesome-skills/fp-ts-react","sourceUrl":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-ts-react","isPrimary":false,"firstSeenAt":"2026-04-18T21:37:32.758Z","lastSeenAt":"2026-04-24T00:50:58.826Z"}],"details":{"listingId":"fb02597c-7d1f-4d0b-9b02-f65c92e91d3e","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"sickn33","slug":"fp-ts-react","github":{"repo":"sickn33/antigravity-awesome-skills","stars":34793,"topics":["agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding","ai-workflows","antigravity","antigravity-skills","claude-code","claude-code-skills","codex-cli","codex-skills","cursor","cursor-skills","developer-tools","gemini-cli","gemini-skills","kiro","mcp","skill-library"],"license":"mit","html_url":"https://github.com/sickn33/antigravity-awesome-skills","pushed_at":"2026-04-24T00:28:59Z","description":"Installable GitHub library of 1,400+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and more. Includes installer CLI, bundles, workflows, and official/community skill collections.","skill_md_sha":"4f9296c9fd5dfc0afb11387857687dda251142cb","skill_md_path":"skills/fp-ts-react/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-ts-react"},"layout":"multi","source":"github","category":"antigravity-awesome-skills","frontmatter":{"name":"fp-ts-react","description":"Practical patterns for using fp-ts with React - hooks, state, forms, data fetching. Use when building React apps with functional programming patterns. Works with React 18/19, Next.js 14/15."},"skills_sh_url":"https://skills.sh/sickn33/antigravity-awesome-skills/fp-ts-react"},"updatedAt":"2026-04-24T00:50:58.826Z"}}