Skip to main content

fp-react

"Implement — Practical patterns for using fp-ts with React - hooks, state, forms, data fetching. Works with React 18/19, Next.js"

Informações da origem

Repositório
thiagofernandes1987-create/APEX
Última atividade na origem
18 de abril de 2026 às 09:35
Idioma detectado do SKILL.md
inglês
Estrelas
2
Forks
0

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
fp-react
description
"Implement — Practical patterns for using fp-ts with React - hooks, state, forms, data fetching. Works with React 18/19, Next.js"
# Functional Programming in React Practical patterns for React apps. No jargon, just code that works. --- ## 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(() =>
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub