{"id":"8fb5c355-2d32-429b-9401-90fbf1004979","shortId":"vMDm2c","kind":"skill","title":"fp-react","tagline":"Practical patterns for using fp-ts with React - hooks, state, forms, data fetching. 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---\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-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-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,559 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.383Z","embedding":null,"createdAt":"2026-04-18T21:37:28.974Z","updatedAt":"2026-04-24T00:50:58.383Z","lastSeenAt":"2026-04-24T00:50:58.383Z","tsv":"'/api/notifications':799 '/api/stats':796 '/api/user':794 '/api/users':724,758,765,1008 '/apply':349,782 '/array':264,1750 '/colinhacks/zod)**':1901 '/devexperts/remote-data-ts)**':1887 '/either':256,342,622,1527 '/eq':1228 '/function':116,202,271,363,629,1291 '/gcanti/fp-ts)**':1870 '/gcanti/io-ts)**':1894 '/mblink/fp-ts-react-stable-hooks)**':1881 '/nonemptyarray':356 '/option':109,195,1220 '/posts':767 '/readertaskeither':1284 '/taskeither':614 '0':320,506,569 '0.5':1673 '1':76,164,1096,1123,1131,1133,1139,1154,1245,1423,1431,1454,1500,1674,1687 '14/15':23 '18/19':21 '19':1457,1463,1514,1614 '2':230,1186,1697 '3':583,1709 '4':807,834 '5':1083 '6':1261 '7':1455 '8':300,309,1676 'a.findfirst':1773 'a.head':1759 'action':1592 'actual':1173,1655 'add':1789 'addoptimistictodo':1627,1653 'address':291 'addtodo':1638 'admin':1776,1778,1781 'adminbadg':1779 'adminus':1770 'alic':166 'alice@example.com':168 'analyt':1317,1377,1408,1434 'analytics.track':1397 'api':64,596,743,773,1306,1376,1407,1418 'api.getuser':1390 'app':32,1302,1496 'appdepend':1305,1329,1338,1352,1417 'applic':1843 'appprovid':1334,1363,1444 'array':1740 'ask':1943 'async':60,589,642,815,971,1442,1534,1568,1639,1823 'await':645,977,1447,1570,1661 'bad':1039,1110 'badcompon':1117 'badstat':1045 'base':1682,1724 'bash':1193 'basic':95,598 'beat':1036 'better':524 'boolean':822,1037,1050,1533 'boundari':1951 'button':160,179,1019,1602 'call':65,597,744,774 'case':935,939,943,948 'catch':982,1401 'chain':73,184,742 'chang':1106,1136,1165,1174,1177,1254 'charact':310 'cheat':1679 'children':1336,1339,1345 'clarif':1945 'classnam':1599 'clear':1918 'clearer':93 'code':36 'collect':327,365 'color':509 'combin':838 'common':1677 'compon':427,580,1270,1368 'condit':1786 'console.log':1134,1175,1252 'const':134,140,147,278,292,311,373,430,437,442,541,634,643,672,679,686,718,752,788,865,874,883,895,961,969,975,1003,1118,1148,1155,1240,1326,1353,1375,1379,1415,1482,1497,1542,1558,1581,1625,1637,1642,1714,1755,1769,1795 'constructor':864 'context':1265,1325,1846 'core':1871 'creat':1097,1111,1324 'createcontext':1293,1328 'creation':1145 'criteria':1954 'crypto.randomuuid':1645 'custom':666,1259 'data':16,244,460,584,673,712,719,735,819,860,899,906,976,981,1046,1056,1314,1321,1475,1543,1562,1735,1737,1738 'default':229,1713 'defin':1298 'dep':1335,1337,1344,1354,1358,1365,1445 'depend':1179,1262,1273,1275,1349,1413,1844 'depscontext':1327,1356 'depscontext.provider':1342 'describ':1922 'devexperts/remote-data-ts':1884 'disabl':1603 'done':1647,1660 'e':251,337,409,410,416,417,423,424,464,471,483,497,548,550,555,557,562,564,617,841,856,867,870,876,879,885,888,890,897,902,913,918,925,926,1402,1405,1522,1595,1598,1601,1821,1828,1834 'e.either':282,296,315,398,538 'e.getapplicativevalidation':376 'e.left':288,303,323,570 'e.mapleft':408,415,422,547,554,561 'e.match':447,1563,1730 'e.preventdefault':465 'e.right':286,301,321,572 'e.target.value':476,488,502 'effect':1246 'either':53,234,235,275,1555,1726,1820,1840 'email':123,167,280,287,290,382,390,411,435,487,490,575,1544,1546 'email.includes':285 'environ':1934 'environment-specif':1933 'eq':1223 'eq.eqnumber':1258 'eq.tuple':1256 'equal':1235,1239,1250,1260 'err':448,450,512,517,657,658,661,665,700,702,983,986,989,993,1024 'err.message':1027 'error':241,329,367,438,453,523,542,567,571,639,652,660,663,680,683,713,720,730,732,821,855,887,894,966,988,991,1051,1052,1060,1074,1361,1384,1530,1564,1565,1574,1587,1600,1731,1733,1734 'error.message':733 'errorlist':1732 'errormessag':276,1025 'errors.email':556,581,582 'errors.length':505 'errors.map':511 'errors.name':549 'errors.password':563 'event':1319 'everi':1114,1127 'exact':833 'execut':970,996,1005,1014,1021,1029 'exist':132,1070,1794,1814 'expect':1451 'expert':1939 'f':473,474,485,486,499,500 'fail':56,63,593,857,1818,1826 'failur':854,884,893,944,985,1081,1404 'fallback':1506 'fals':704,709,1567,1589,1648 'featur':1762 'feedback':1612 'fetch':17,585,599,631,646,746,749,783,1007,1476,1502 'fetchdashboarddata':789 'fetchfn':959,978 'fetchjson':635,697,757,763,793,795,797 'fetchus':1499 'fetchuserwithpost':753 'field':521 'field-level':520 'fielderror':528,539,543 'find':1766 'first':332,371,1752 'firstus':1756 'fold':912,1017,1698 'form':15,57,231,246,396,431,446,462,536,1512,1591,1836 'form.email':414,481,553,576 'form.name':407,469,546 'form.name.trim':574 'form.password':421,495,560,578 'formact':1583,1593 'formdata':1539,1540 'formdata.get':1545,1550 'formstat':1529,1538 'fp':2,9,107,114,193,200,254,262,269,340,347,354,361,612,620,627,780,1091,1188,1197,1208,1218,1226,1233,1282,1289,1525,1748,1854,1859,1866,1874,1908 'fp-react':1 'fp-ts':8,106,113,192,199,253,261,268,339,346,353,360,611,619,626,779,1090,1217,1225,1232,1281,1288,1524,1747,1853,1865,1907 'fp-ts-react-stable-hook':1187,1196,1207,1858,1873 'function':24,125,213,273,394,428,534,668,716,911,957,998,1116,1146,1229,1333,1350,1370,1477,1495,1535,1579,1620 'get':1751 'getorels':1710 'getsemigroup':351,377 'getthem':214 'getus':1307,1419 'github.com':1869,1880,1886,1893,1900 'github.com/colinhacks/zod)**':1899 'github.com/devexperts/remote-data-ts)**':1885 'github.com/gcanti/fp-ts)**':1868 'github.com/gcanti/io-ts)**':1892 'github.com/mblink/fp-ts-react-stable-hooks)**':1879 'good':1066,1142 'goodcompon':1147 'goodstat':1072 'got':862 'great':1905 'guest':1722 'handl':814 'handlelogin':141,162 'handlelogout':148,181 'handlesubmit':443,466 'haven':845 'hook':13,600,667,953,1192,1201,1212,1346,1863,1878,1883 'http':653 'id':119,163,1308,1312,1422,1430,1644 'immedi':1503,1649 'import':98,102,110,188,196,249,257,265,335,343,350,357,602,607,615,623,776,1203,1213,1221,1277,1285,1292,1465,1516,1520,1616,1743 'imposs':837,1040 'inject':1263,1272,1845 'input':467,479,491,1948 'instal':1195 'instanceof':659,987 'instant':1611 'instead':88,1236,1470 'intent':94 'interfac':117,203,378,386,1044,1304,1528 'invalid':289 'io':1890 'io-t':1889 'isopen':1797 'ispend':1584,1604,1605 'item':1753,1768 'jargon':34 'jest.fn':1420,1428,1436 'key':515,739,1597,1668 'keyof':531 'least':308 'left':240 'length':319,568 'level':522 'li':514,738,1667 'librari':1864,1872 'light':228 'like':1094 'limit':1910 'load':51,687,714,721,726,728,820,850,875,882,940,973,1022,1049,1058,1080,1389,1438 'loading/error/success':70,1831 'log':169,182 'machin':827 'map':736 'match':908,1688,1701,1919 'mayb':80,84 'maybetitl':1800 'maybeus':1690,1707,1717 'mean':1065 'memoiz':1143 'messag':1026 'might':46,55,62,130,592,1812,1817,1825 'miss':48,1362,1956 'mock':1412 'mockdep':1416,1446 'mockdeps.api.getuser':1452 'mockresolvedvalu':1421,1429 'modalprop':1796 'mount':1441 'multipl':74,784,1838 'must':305 'name':121,165,207,313,324,380,388,404,434,475,478,573,1424,1432,1715 'name.trim':318,322 'need':67,1303,1830 'new':651,662,990,1098,1112,1360 'newtodo':1631,1634,1643,1654,1663 'next.js':22 'notask':844,866,873,936,968,1079,1386 'notif':798,803,806 'npm':1194 'null':90,677,678,684,685,695,1048,1053,1153,1330,1331,1785 'number':1152 'o':104,190,1215 'o.flatmap':221 'o.fold':1702 'o.fromnullable':1158 'o.geteq':1257 'o.getorelse':227,1721,1765,1784,1804 'o.map':224,1718,1760,1777,1801 'o.match':154,1691 'o.none':139,150 'o.option':138,206,210 'o.some':145,1095,1122,1130,1132,1244 'o.tonullable':1782 'object':1099,1322 'object.keys':566 'onchang':470,482,496 'onclick':161,180,1020 'one':333,372 'onfailur':924,946 'onload':922,942 'onnotask':920,938 'onretri':1028 'onsubmit':463 'onsuccess':928,951 'opac':1671 'oper':54,61,590,1816,1824 'optimistictodo':1626 'optimistictodos.map':1665 'option':44,79,87,127,185,1113,1144,1184,1249,1684,1815 'output':1928 'p':1596 'parallel':772 'parent':1491 'partial':529,1315 'password':294,302,304,384,392,418,436,493,501,504,577,1549,1551 'password.length':299 'pattern':5,29,41,96,809,829,907,1458,1678,1686,1696,1708 'pend':1635 'perfect':237,594 'permiss':1949 'pipe':72,111,152,197,219,266,358,402,405,412,419,444,544,551,558,624,696,756,762,790,1286,1560,1689,1716,1728,1757,1771,1799 'placehold':477,489,503 'possibl':1043 'post':751,764,769,771 'practic':4,28 'prevent':1086,1848 'prevstat':1537 'problem':1108 'profil':204,215,216 'profile.user':220 'program':25 'progress':852 'promis':960,1310,1316,1461,1481,1494,1541 'prop':1787,1790 'proper':825 'provid':1332,1492 'quick':39 'r':915,921,923,927,931,932,1011 'r.json':1012 'raw':1181 'rawvalu':1149,1159,1160,1164,1172,1178 'rd':916 'rd._tag':934 'rd.data':952 'rd.error':947 're':1088,1850 're-rend':1087,1849 'react':3,12,20,27,31,101,606,1102,1190,1199,1210,1297,1456,1462,1469,1513,1519,1613,1619,1861,1876 'reactnod':1295,1340 'readertaskeith':1267,1847 'reconcil':1658 'record':530 'recreat':1162 'red':510 'refer':40,1238 'referenti':1084 'remotedata':66,808,831,840,869,878,889,901,917,955,965,1035,1073,1383,1833,1888 'render':1089,1101,1115,1128,1409,1443,1681,1723,1741,1851 'requir':326,1947 'res':644 'res.json':656 'res.ok':649 'res.status':654 'resolv':1488 'result':705,707,1559,1578 'return':151,218,274,401,461,565,655,711,727,731,734,937,941,945,950,994,1016,1341,1364,1489,1504,1573,1577,1590,1664 'review':1940 'right':242,811 'rte':1279 'run':1126,1170 'runtim':1895 's.theme':226 'safe':1739,1754 'safeti':1950 'save':1656 'savetodo':1662 'savetoserv':1571 'say':128 'schema':1902 'scope':1921 'screen.findbytext':1448 'see':1103 'sequenc':344,375 'sequencet':777,791 'servic':1300 'set':209 'setdata':674,706 'seterror':439,449,455,681,694,701 'setform':432,472,484,498 'setload':688,692,703,708 'setrawvalu':1150 'setstat':963,972,979,984 'setup':1274 'setus':136,144,149,1381,1388,1394,1403 'setvalu':1120,1242 'sheet':1680 'show':69,451,1650 'sign':518,1607 'signupform':379,397,429,532,537,1580 'simpl':245,1712 'situat':1809 'skill':1913 'skill-fp-react' 'solut':1138,1185 'someerror':1061 'source-sickn33' 'specif':1767,1935 'stabil':1085 'stabl':1191,1200,1211,1862,1877,1882 'stablecompon':1230 'start':847,1501 'stat':802,805 'state':14,71,77,816,826,835,910,962,995,1004,1018,1041,1069,1582,1630,1633 'state.errors.map':1594 'stop':817,1941 'string':120,122,124,208,212,217,281,283,284,295,297,298,314,316,317,381,383,385,389,391,393,399,441,533,637,664,671,755,992,1002,1309,1313,1320,1374,1531,1548,1553,1641 'style':508,1670 'submit':458,1606 'submitform':1536,1586 'submittoserv':456 'substitut':1931 'success':859,896,905,949,980,1082,1395,1532,1566,1575,1588,1953 'successmessag':1736 'suspend':1486 'suspens':1467,1505 'switch':933 'sync':1819 'tag':843,849,853,858,872,881,892,904 'task':1917 'taskeith':59,587,588,633,1827 'te':609 'te.applypar':792 'te.flatmap':760 'te.map':768,800 'te.match':699 'te.taskeither':638 'te.trycatch':641 'test':1410,1425,1437,1449,1937 'testabl':1269 'text':1640,1646 'theme':211 'thing':785 'throw':650,1359 'titl':1802,1803 'todo':1622,1623,1624,1629,1632,1666 'todo.id':1669 'todo.pending':1672 'todo.text':1675 'todolist':1621 'tohavebeencalledwith':1453 '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':1318,1435 'transform':75 'treat':1926 'tri':974 'true':690,693,1059,1576,1636,1798 'ts':10,108,115,194,201,255,263,270,341,348,355,362,613,621,628,781,1092,1189,1198,1209,1219,1227,1234,1283,1290,1526,1749,1855,1860,1867,1875,1891,1909 'type':492,527,839,1071,1896 'typescript':97,187,248,334,526,601,745,775,830,956,1038,1109,1141,1202,1276,1369,1414,1464,1515,1615,1685,1727,1742,1788 'u':176,222,1393,1396,1719,1774 'u.name':178,1720 'u.role':1775 'u.settings':223 'ui':1652,1832 'ul':507 'undefin':91 'understand':1248 'updat':1433 'updateus':1311,1427 'url':636,647,670,698,710 'usag':425,715,997 'use':7,42,86,818,823,1231,1266,1348,1366,1459,1466,1484,1554,1807,1810,1911 'useactionst':1510,1517,1585 'usecontext':1294,1355 'usedep':1351,1378 'useeffect':604,691,1013,1124,1166,1387,1472 'usefetch':669,722 'usememo':1140,1157,1856 'useoptimist':1609,1617,1628 'user':49,118,135,143,153,159,175,205,723,737,747,761,770,801,804,1023,1030,1032,1033,1047,1057,1075,1380,1385,1398,1426,1439,1450,1483,1692,1694,1695,1703,1705,1706,1758,1761,1763,1764,1772,1780 'user.id':740 'user.name':741,1490 'usercard':1031 'userdata':142,146 'useremotedata':958,1006 'userid':754,759,766,1000,1001,1009,1015,1372,1373,1391,1400,1406 'userlist':717 'usermenu':1693,1704 'userprofil':126,999,1371,1478,1507 'userpromis':1479,1480,1485,1498,1508,1509 'usest':99,137,433,440,603,675,682,689,964,1121,1151,1382,1473 'usestableeffect':1205,1251 'usestableo':1204,1243 'ux':525 'valid':58,232,239,243,247,272,454,457,459,1068,1557,1569,1572,1839,1842,1897,1903,1936 'validateal':374,403 'validatedform':387,400,540 'validateemail':279,413,552 'validateform':395,445,1561 'validateformwithfielderror':535 'validatenam':312,406,545 'validatepassword':293,420,559 'validationresult':1729 'validvalu':277 'valu':45,186,468,480,494,1093,1119,1135,1137,1156,1176,1182,1241,1253,1255,1343,1793,1811 'view':1399 'void':1323 'way':812 'welcom':177 'work':18,38,1904 'wrap':630 'yet':52,133,848 'zod':1898","prices":[{"id":"69fea51f-f6c1-443b-8ea2-9fe5f4d4f9ad","listingId":"8fb5c355-2d32-429b-9401-90fbf1004979","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:28.974Z"}],"sources":[{"listingId":"8fb5c355-2d32-429b-9401-90fbf1004979","source":"github","sourceId":"sickn33/antigravity-awesome-skills/fp-react","sourceUrl":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-react","isPrimary":false,"firstSeenAt":"2026-04-18T21:37:28.974Z","lastSeenAt":"2026-04-24T00:50:58.383Z"}],"details":{"listingId":"8fb5c355-2d32-429b-9401-90fbf1004979","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"sickn33","slug":"fp-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":"a804fda8e3ff7e5ba46cde3555b6858991ef4d82","skill_md_path":"skills/fp-react/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-react"},"layout":"multi","source":"github","category":"antigravity-awesome-skills","frontmatter":{"name":"fp-react","description":"Practical patterns for using fp-ts with React - hooks, state, forms, data fetching. Works with React 18/19, Next.js 14/15."},"skills_sh_url":"https://skills.sh/sickn33/antigravity-awesome-skills/fp-react"},"updatedAt":"2026-04-24T00:50:58.383Z"}}