- Add date_added to all 950+ skills for complete tracking - Update version to 6.5.0 in package.json and README - Regenerate all indexes and catalog - Sync all generated files Features from merged PR #150: - Stars/Upvotes system for community-driven discovery - Auto-update mechanism via START_APP.bat - Interactive Prompt Builder - Date tracking badges - Smart auto-categorization All skills validated and indexed. Made-with: Cursor
798 lines
18 KiB
Markdown
798 lines
18 KiB
Markdown
---
|
|
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."
|
|
risk: safe
|
|
source: "https://github.com/whatiskadudoing/fp-ts-skills"
|
|
date_added: "2026-02-27"
|
|
---
|
|
|
|
# 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)
|