{
  "schema": "https://ai-atoms.com/schemas/skill-v1.json",
  "type": "skill",
  "id": "skill/fp-ts-react",
  "version": "1.0.0",
  "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.",
  "system_prompt_fragment": "# 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)",
  "applicable_domains": [
    "frontend"
  ],
  "category": "frontend",
  "invocation": [
    "/fp-ts-react"
  ],
  "authored_by": "claudeskills.in community",
  "source_url": "https://claudeskills.in/skill/fp-ts-react",
  "provenance": {
    "source": "claudeskills.in",
    "source_url": "https://claudeskills.in/skill/fp-ts-react",
    "license": "unknown",
    "imported_at": "2026-09-03",
    "notes": "Aggregated by claudeskills.in from community GitHub lists. Upstream as recorded by the aggregator: https://github.com/whatiskadudoing/fp-ts-skills."
  },
  "tags": [
    "claudeskills",
    "frontend",
    "risk-reviewed"
  ],
  "lifecycle": "draft"
}