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"

الانتقال إلى التثبيت

معلومات المصدر

المستودع
thiagofernandes1987-create/APEX
آخر نشاط في المصدر
١٨ أبريل ٢٠٢٦ في ٠٩:٣٥
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٢
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
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(() =>
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub