feat: add fp-ts skills for TypeScript functional programming (#43)
Add three practical fp-ts skills: - fp-ts-pragmatic: The 80/20 of functional programming, jargon-free - fp-ts-react: Patterns for using fp-ts with React 18/19 and Next.js - fp-ts-errors: Type-safe error handling with Either and TaskEither Source: https://github.com/whatiskadudoing/fp-ts-skills Co-authored-by: kadu-maverickk <maycon.guedes@itglobers.com>
This commit is contained in:
796
skills/fp-ts-react/SKILL.md
Normal file
796
skills/fp-ts-react/SKILL.md
Normal file
@@ -0,0 +1,796 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
# 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)
|
||||
Reference in New Issue
Block a user