fp-ts-react
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.
Best use case
fp-ts-react is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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.
Teams using fp-ts-react should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/fp-ts-react/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How fp-ts-react Compares
| Feature / Agent | fp-ts-react | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
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.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Functional Programming in React
Practical patterns for React apps. No jargon, just code that works.
## When to Use This Skill
- When building React apps with fp-ts for type-safe state management
- When handling loading/error/success states in data fetching
- When implementing form validation with error accumulation
- When using React 18/19 or Next.js 14/15 with functional patterns
---
## Quick Reference
| Pattern | Use When |
|---------|----------|
| `Option` | Value might be missing (user not loaded yet) |
| `Either` | Operation might fail (form validation) |
| `TaskEither` | Async operation might fail (API calls) |
| `RemoteData` | Need to show loading/error/success states |
| `pipe` | Chaining multiple transformations |
---
## 1. State with Option (Maybe It's There, Maybe Not)
Use `Option` instead of `null | undefined` for clearer intent.
### Basic Pattern
```typescript
import { useState } from 'react'
import * as O from 'fp-ts/Option'
import { pipe } from 'fp-ts/function'
interface User {
id: string
name: string
email: string
}
function UserProfile() {
// Option says "this might not exist yet"
const [user, setUser] = useState<O.Option<User>>(O.none)
const handleLogin = (userData: User) => {
setUser(O.some(userData))
}
const handleLogout = () => {
setUser(O.none)
}
return pipe(
user,
O.match(
// When there's no user
() => <button onClick={() => handleLogin({ id: '1', name: 'Alice', email: 'alice@example.com' })}>
Log In
</button>,
// When there's a user
(u) => (
<div>
<p>Welcome, {u.name}!</p>
<button onClick={handleLogout}>Log Out</button>
</div>
)
)
)
}
```
### Chaining Optional Values
```typescript
import * as O from 'fp-ts/Option'
import { pipe } from 'fp-ts/function'
interface Profile {
user: O.Option<{
name: string
settings: O.Option<{
theme: string
}>
}>
}
function getTheme(profile: Profile): string {
return pipe(
profile.user,
O.flatMap(u => u.settings),
O.map(s => s.theme),
O.getOrElse(() => 'light') // default
)
}
```
---
## 2. Form Validation with Either
Either is perfect for validation: `Left` = errors, `Right` = valid data.
### Simple Form Validation
```typescript
import * as E from 'fp-ts/Either'
import * as A from 'fp-ts/Array'
import { pipe } from 'fp-ts/function'
// Validation functions return Either<ErrorMessage, ValidValue>
const validateEmail = (email: string): E.Either<string, string> =>
email.includes('@')
? E.right(email)
: E.left('Invalid email address')
const validatePassword = (password: string): E.Either<string, string> =>
password.length >= 8
? E.right(password)
: E.left('Password must be at least 8 characters')
const validateName = (name: string): E.Either<string, string> =>
name.trim().length > 0
? E.right(name.trim())
: E.left('Name is required')
```
### Collecting All Errors (Not Just First One)
```typescript
import * as E from 'fp-ts/Either'
import { sequenceS } from 'fp-ts/Apply'
import { getSemigroup } from 'fp-ts/NonEmptyArray'
import { pipe } from 'fp-ts/function'
// This collects ALL errors, not just the first one
const validateAll = sequenceS(E.getApplicativeValidation(getSemigroup<string>()))
interface SignupForm {
name: string
email: string
password: string
}
interface ValidatedForm {
name: string
email: string
password: string
}
function validateForm(form: SignupForm): E.Either<string[], ValidatedForm> {
return pipe(
validateAll({
name: pipe(validateName(form.name), E.mapLeft(e => [e])),
email: pipe(validateEmail(form.email), E.mapLeft(e => [e])),
password: pipe(validatePassword(form.password), E.mapLeft(e => [e])),
})
)
}
// Usage in component
function SignupForm() {
const [form, setForm] = useState({ name: '', email: '', password: '' })
const [errors, setErrors] = useState<string[]>([])
const handleSubmit = () => {
pipe(
validateForm(form),
E.match(
(errs) => setErrors(errs), // Show all errors
(valid) => {
setErrors([])
submitToServer(valid) // Submit valid data
}
)
)
}
return (
<form onSubmit={e => { e.preventDefault(); handleSubmit() }}>
<input
value={form.name}
onChange={e => setForm(f => ({ ...f, name: e.target.value }))}
placeholder="Name"
/>
<input
value={form.email}
onChange={e => setForm(f => ({ ...f, email: e.target.value }))}
placeholder="Email"
/>
<input
type="password"
value={form.password}
onChange={e => setForm(f => ({ ...f, password: e.target.value }))}
placeholder="Password"
/>
{errors.length > 0 && (
<ul style={{ color: 'red' }}>
{errors.map((err, i) => <li key={i}>{err}</li>)}
</ul>
)}
<button type="submit">Sign Up</button>
</form>
)
}
```
### Field-Level Errors (Better UX)
```typescript
type FieldErrors = Partial<Record<keyof SignupForm, string>>
function validateFormWithFieldErrors(form: SignupForm): E.Either<FieldErrors, ValidatedForm> {
const errors: FieldErrors = {}
pipe(validateName(form.name), E.mapLeft(e => { errors.name = e }))
pipe(validateEmail(form.email), E.mapLeft(e => { errors.email = e }))
pipe(validatePassword(form.password), E.mapLeft(e => { errors.password = e }))
return Object.keys(errors).length > 0
? E.left(errors)
: E.right({ name: form.name.trim(), email: form.email, password: form.password })
}
// In component
{errors.email && <span className="error">{errors.email}</span>}
```
---
## 3. Data Fetching with TaskEither
TaskEither = async operation that might fail. Perfect for API calls.
### Basic Fetch Hook
```typescript
import { useState, useEffect } from 'react'
import * as TE from 'fp-ts/TaskEither'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
// Wrap fetch in TaskEither
const fetchJson = <T>(url: string): TE.TaskEither<Error, T> =>
TE.tryCatch(
async () => {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
},
(err) => err instanceof Error ? err : new Error(String(err))
)
// Custom hook
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<Error | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(true)
setError(null)
pipe(
fetchJson<T>(url),
TE.match(
(err) => {
setError(err)
setLoading(false)
},
(result) => {
setData(result)
setLoading(false)
}
)
)()
}, [url])
return { data, error, loading }
}
// Usage
function UserList() {
const { data, error, loading } = useFetch<User[]>('/api/users')
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data?.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}
```
### Chaining API Calls
```typescript
// Fetch user, then fetch their posts
const fetchUserWithPosts = (userId: string) => pipe(
fetchJson<User>(`/api/users/${userId}`),
TE.flatMap(user => pipe(
fetchJson<Post[]>(`/api/users/${userId}/posts`),
TE.map(posts => ({ ...user, posts }))
))
)
```
### Parallel API Calls
```typescript
import { sequenceT } from 'fp-ts/Apply'
// Fetch multiple things at once
const fetchDashboardData = () => pipe(
sequenceT(TE.ApplyPar)(
fetchJson<User>('/api/user'),
fetchJson<Stats>('/api/stats'),
fetchJson<Notifications[]>('/api/notifications')
),
TE.map(([user, stats, notifications]) => ({
user,
stats,
notifications
}))
)
```
---
## 4. RemoteData Pattern (The Right Way to Handle Async State)
Stop using `{ data, loading, error }` booleans. Use a proper state machine.
### The Pattern
```typescript
// RemoteData has exactly 4 states - no impossible combinations
type RemoteData<E, A> =
| { _tag: 'NotAsked' } // Haven't started yet
| { _tag: 'Loading' } // In progress
| { _tag: 'Failure'; error: E } // Failed
| { _tag: 'Success'; data: A } // Got it!
// Constructors
const notAsked = <E, A>(): RemoteData<E, A> => ({ _tag: 'NotAsked' })
const loading = <E, A>(): RemoteData<E, A> => ({ _tag: 'Loading' })
const failure = <E, A>(error: E): RemoteData<E, A> => ({ _tag: 'Failure', error })
const success = <E, A>(data: A): RemoteData<E, A> => ({ _tag: 'Success', data })
// Pattern match all states
function fold<E, A, R>(
rd: RemoteData<E, A>,
onNotAsked: () => R,
onLoading: () => R,
onFailure: (e: E) => R,
onSuccess: (a: A) => R
): R {
switch (rd._tag) {
case 'NotAsked': return onNotAsked()
case 'Loading': return onLoading()
case 'Failure': return onFailure(rd.error)
case 'Success': return onSuccess(rd.data)
}
}
```
### Hook with RemoteData
```typescript
function useRemoteData<T>(fetchFn: () => Promise<T>) {
const [state, setState] = useState<RemoteData<Error, T>>(notAsked())
const execute = async () => {
setState(loading())
try {
const data = await fetchFn()
setState(success(data))
} catch (err) {
setState(failure(err instanceof Error ? err : new Error(String(err))))
}
}
return { state, execute }
}
// Usage
function UserProfile({ userId }: { userId: string }) {
const { state, execute } = useRemoteData(() =>
fetch(`/api/users/${userId}`).then(r => r.json())
)
useEffect(() => { execute() }, [userId])
return fold(
state,
() => <button onClick={execute}>Load User</button>,
() => <Spinner />,
(err) => <ErrorMessage message={err.message} onRetry={execute} />,
(user) => <UserCard user={user} />
)
}
```
### Why RemoteData Beats Booleans
```typescript
// ❌ BAD: Impossible states are possible
interface BadState {
data: User | null
loading: boolean
error: Error | null
}
// Can have: { data: user, loading: true, error: someError } - what does that mean?!
// ✅ GOOD: Only valid states exist
type GoodState = RemoteData<Error, User>
// Can only be: NotAsked | Loading | Failure | Success
```
---
## 5. Referential Stability (Preventing Re-renders)
fp-ts values like `O.some(1)` create new objects each render. React sees them as "changed".
### The Problem
```typescript
// ❌ BAD: Creates new Option every render
function BadComponent() {
const [value, setValue] = useState(O.some(1))
useEffect(() => {
// This runs EVERY render because O.some(1) !== O.some(1)
console.log('value changed')
}, [value])
}
```
### Solution 1: useMemo
```typescript
// ✅ GOOD: Memoize Option creation
function GoodComponent() {
const [rawValue, setRawValue] = useState<number | null>(1)
const value = useMemo(
() => O.fromNullable(rawValue),
[rawValue] // Only recreate when rawValue changes
)
useEffect(() => {
// Now this only runs when rawValue actually changes
console.log('value changed')
}, [rawValue]) // Depend on raw value, not Option
}
```
### Solution 2: fp-ts-react-stable-hooks
```bash
npm install fp-ts-react-stable-hooks
```
```typescript
import { useStableO, useStableEffect } from 'fp-ts-react-stable-hooks'
import * as O from 'fp-ts/Option'
import * as Eq from 'fp-ts/Eq'
function StableComponent() {
// Uses fp-ts equality instead of reference equality
const [value, setValue] = useStableO(O.some(1))
// Effect that understands Option equality
useStableEffect(
() => { console.log('value changed') },
[value],
Eq.tuple(O.getEq(Eq.eqNumber)) // Custom equality
)
}
```
---
## 6. Dependency Injection with Context
Use ReaderTaskEither for testable components with injected dependencies.
### Setup Dependencies
```typescript
import * as RTE from 'fp-ts/ReaderTaskEither'
import { pipe } from 'fp-ts/function'
import { createContext, useContext, ReactNode } from 'react'
// Define what services your app needs
interface AppDependencies {
api: {
getUser: (id: string) => Promise<User>
updateUser: (id: string, data: Partial<User>) => Promise<User>
}
analytics: {
track: (event: string, data?: object) => void
}
}
// Create context
const DepsContext = createContext<AppDependencies | null>(null)
// Provider
function AppProvider({ deps, children }: { deps: AppDependencies; children: ReactNode }) {
return <DepsContext.Provider value={deps}>{children}</DepsContext.Provider>
}
// Hook to use dependencies
function useDeps(): AppDependencies {
const deps = useContext(DepsContext)
if (!deps) throw new Error('Missing AppProvider')
return deps
}
```
### Use in Components
```typescript
function UserProfile({ userId }: { userId: string }) {
const { api, analytics } = useDeps()
const [user, setUser] = useState<RemoteData<Error, User>>(notAsked())
useEffect(() => {
setUser(loading())
api.getUser(userId)
.then(u => {
setUser(success(u))
analytics.track('user_viewed', { userId })
})
.catch(e => setUser(failure(e)))
}, [userId, api, analytics])
// render...
}
```
### Testing with Mock Dependencies
```typescript
const mockDeps: AppDependencies = {
api: {
getUser: jest.fn().mockResolvedValue({ id: '1', name: 'Test User' }),
updateUser: jest.fn().mockResolvedValue({ id: '1', name: 'Updated' }),
},
analytics: {
track: jest.fn(),
},
}
test('loads user on mount', async () => {
render(
<AppProvider deps={mockDeps}>
<UserProfile userId="1" />
</AppProvider>
)
await screen.findByText('Test User')
expect(mockDeps.api.getUser).toHaveBeenCalledWith('1')
})
```
---
## 7. React 19 Patterns
### use() for Promises (React 19+)
```typescript
import { use, Suspense } from 'react'
// Instead of useEffect + useState for data fetching
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise) // Suspends until resolved
return <div>{user.name}</div>
}
// Parent provides the promise
function App() {
const userPromise = fetchUser('1') // Start fetching immediately
return (
<Suspense fallback={<Spinner />}>
<UserProfile userPromise={userPromise} />
</Suspense>
)
}
```
### useActionState for Forms (React 19+)
```typescript
import { useActionState } from 'react'
import * as E from 'fp-ts/Either'
interface FormState {
errors: string[]
success: boolean
}
async function submitForm(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
}
// Use Either for validation
const result = pipe(
validateForm(data),
E.match(
(errors) => ({ errors, success: false }),
async (valid) => {
await saveToServer(valid)
return { errors: [], success: true }
}
)
)
return result
}
function SignupForm() {
const [state, formAction, isPending] = useActionState(submitForm, {
errors: [],
success: false
})
return (
<form action={formAction}>
<input name="email" type="email" />
<input name="password" type="password" />
{state.errors.map(e => <p key={e} className="error">{e}</p>)}
<button disabled={isPending}>
{isPending ? 'Submitting...' : 'Sign Up'}
</button>
</form>
)
}
```
### useOptimistic for Instant Feedback (React 19+)
```typescript
import { useOptimistic } from 'react'
function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, { ...newTodo, pending: true }]
)
const addTodo = async (text: string) => {
const newTodo = { id: crypto.randomUUID(), text, done: false }
// Immediately show in UI
addOptimisticTodo(newTodo)
// Actually save (will reconcile when done)
await saveTodo(newTodo)
}
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.text}
</li>
))}
</ul>
)
}
```
---
## 8. Common Patterns Cheat Sheet
### Render Based on Option
```typescript
// Pattern 1: match
pipe(
maybeUser,
O.match(
() => <LoginButton />,
(user) => <UserMenu user={user} />
)
)
// Pattern 2: fold (same as match)
O.fold(
() => <LoginButton />,
(user) => <UserMenu user={user} />
)(maybeUser)
// Pattern 3: getOrElse for simple defaults
const name = pipe(
maybeUser,
O.map(u => u.name),
O.getOrElse(() => 'Guest')
)
```
### Render Based on Either
```typescript
pipe(
validationResult,
E.match(
(errors) => <ErrorList errors={errors} />,
(data) => <SuccessMessage data={data} />
)
)
```
### Safe Array Rendering
```typescript
import * as A from 'fp-ts/Array'
// Get first item safely
const firstUser = pipe(
users,
A.head,
O.map(user => <Featured user={user} />),
O.getOrElse(() => <NoFeaturedUser />)
)
// Find specific item
const adminUser = pipe(
users,
A.findFirst(u => u.role === 'admin'),
O.map(admin => <AdminBadge user={admin} />),
O.toNullable // or O.getOrElse(() => null)
)
```
### Conditional Props
```typescript
// Add props only if value exists
const modalProps = {
isOpen: true,
...pipe(
maybeTitle,
O.map(title => ({ title })),
O.getOrElse(() => ({}))
)
}
```
---
## When to Use What
| Situation | Use |
|-----------|-----|
| Value might not exist | `Option<T>` |
| Operation might fail (sync) | `Either<E, A>` |
| Async operation might fail | `TaskEither<E, A>` |
| Need loading/error/success UI | `RemoteData<E, A>` |
| Form with multiple validations | `Either` with validation applicative |
| Dependency injection | Context + `ReaderTaskEither` |
| Prevent re-renders with fp-ts | `useMemo` or `fp-ts-react-stable-hooks` |
---
## Libraries
- **[fp-ts](https://github.com/gcanti/fp-ts)** - Core library
- **[fp-ts-react-stable-hooks](https://github.com/mblink/fp-ts-react-stable-hooks)** - Stable hooks
- **[@devexperts/remote-data-ts](https://github.com/devexperts/remote-data-ts)** - RemoteData
- **[io-ts](https://github.com/gcanti/io-ts)** - Runtime type validation
- **[zod](https://github.com/colinhacks/zod)** - Schema validation (works great with fp-ts)Related Skills
dotnet-to-react-python-refactor
Agent skill for refactoring .NET applications into a React frontend + Python backend. Use for migrating/modernizing .NET apps (ASP.NET MVC, Web API, Blazor, Web Forms) to React + Python, or analyzing .NET codebases for migration planning.
benefriches-react-best-practices
React best practices and performance optimization for Benefriches (Vite + Redux). Reference when writing components, implementing Redux patterns, reviewing code quality, or optimizing performance.
architectural-patterns-large-react
Establish scalable architecture using modular boundaries, domain-driven design, and consistent data access patterns.
arcanea-react-best-practices
React 19 and Next.js 16 performance optimization for the Arcanea platform. Use when writing, reviewing, or refactoring React components, data fetching logic, bundle optimization, or any frontend performance work. Triggers on: React components, Next.js pages, hooks, data fetching, bundle size, re-renders, Server Components, Client Components, hydration. Sourced from Vercel Engineering's official React best practices (57 rules, 8 categories) and adapted for the Arcanea stack.
agent-react-specialist
Expert React specialist mastering React 18+ with modern patterns and ecosystem. Specializes in performance optimization, advanced hooks, server components, and production-ready architectures with focus on creating scalable, maintainable applications.
Advanced React Clean Integration
Integrate React with clean architecture without framework leakage using hooks as adapters and presenters. Use when connecting React to domain logic, designing hook-based DI, or isolating UI from business rules.
add-reaction
Slack メッセージにリアクションを追加する。「リアクション追加」「リアクションつけて」「👍つけて」「絵文字で反応」「リアクションで返信」「いいねして」「リアクション送って」などで起動。User Token があればユーザーとしてリアクション、なければ Bot としてリアクション。
ant-design-react
Provides comprehensive guidance for Ant Design React component library including components, design system, themes, and TypeScript support. Use when the user asks about Ant Design for React, needs to build React applications with Ant Design, or implement design system patterns.
Error Boundaries React
Error boundaries are React components that catch JavaScript errors anywhere in their component tree, log those errors, and display a fallback UI instead of a component tree that crashed. Introduced in
bgo
Automates the complete Blender build-go workflow, from building and packaging your extension/add-on to removing old versions, installing, enabling, and launching Blender for quick testing and iteration.
frontend-design
Create distinctive, production-grade frontend interfaces with intentional aesthetics, high craft, and non-generic visual identity. Use when building or styling web UIs, components, pages, dashboard...
frontend-design-pro
Creates jaw-dropping, production-ready frontend interfaces AND delivers perfectly matched real photos (Unsplash/Pexels direct links) OR flawless custom image-generation prompts for hero images, backgrounds, and illustrations. Zero AI slop, zero fake URLs.