[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-6cbbead3-7da0-438d-981f-9f64ec1b37fa":3,"$fyGCzOfrVYIjiUE0beDjjyPGozZ3gekpKOSSM3s5Y9nw":43},{"id":4,"title":5,"description":6,"categoryId":7,"moduleId":8,"tags":9,"prompt":10,"icon":11,"source":12,"sourceUrl":13,"authorId":14,"authorName":15,"isPublic":16,"stars":17,"runs":18,"createdAt":19,"updatedAt":19,"module":20,"category":27,"packages":34},"6cbbead3-7da0-438d-981f-9f64ec1b37fa","fp-react","使用fp-ts与React的实用模式 - 钩子、状态、表单、数据获取。兼容React 18\u002F19、Next.js 14\u002F15。","cat_coding_frontend","mod_coding","sickn33,coding","---\nname: fp-react\ndescription: Practical patterns for using fp-ts with React - hooks, state, forms, data fetching. Works with React 18\u002F19, Next.js 14\u002F15.\nrisk: unknown\nsource: community\nversion: 2.0.0\nauthor: fp-ts-skills\ntags: [fp-ts, react, typescript, hooks, state-management, forms, data-fetching, remote-data, react-19, next-js]\n---\n\n# Functional Programming in React\n\nPractical patterns for React apps. No jargon, just code that works.\n\n---\n\n## Quick Reference\n\n| Pattern | Use When |\n|---------|----------|\n| `Option` | Value might be missing (user not loaded yet) |\n| `Either` | Operation might fail (form validation) |\n| `TaskEither` | Async operation might fail (API calls) |\n| `RemoteData` | Need to show loading\u002Ferror\u002Fsuccess states |\n| `pipe` | Chaining multiple transformations |\n\n---\n\n## 1. State with Option (Maybe It's There, Maybe Not)\n\nUse `Option` instead of `null | undefined` for clearer intent.\n\n### Basic Pattern\n\n```typescript\nimport { useState } from 'react'\nimport * as O from 'fp-ts\u002FOption'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\ninterface User {\n  id: string\n  name: string\n  email: string\n}\n\nfunction UserProfile() {\n  \u002F\u002F Option says \"this might not exist yet\"\n  const [user, setUser] = useState\u003CO.Option\u003CUser>>(O.none)\n\n  const handleLogin = (userData: User) => {\n    setUser(O.some(userData))\n  }\n\n  const handleLogout = () => {\n    setUser(O.none)\n  }\n\n  return pipe(\n    user,\n    O.match(\n      \u002F\u002F When there's no user\n      () => \u003Cbutton onClick={() => handleLogin({ id: '1', name: 'Alice', email: 'alice@example.com' })}>\n        Log In\n      \u003C\u002Fbutton>,\n      \u002F\u002F When there's a user\n      (u) => (\n        \u003Cdiv>\n          \u003Cp>Welcome, {u.name}!\u003C\u002Fp>\n          \u003Cbutton onClick={handleLogout}>Log Out\u003C\u002Fbutton>\n        \u003C\u002Fdiv>\n      )\n    )\n  )\n}\n```\n\n### Chaining Optional Values\n\n```typescript\nimport * as O from 'fp-ts\u002FOption'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\ninterface Profile {\n  user: O.Option\u003C{\n    name: string\n    settings: O.Option\u003C{\n      theme: string\n    }>\n  }>\n}\n\nfunction getTheme(profile: Profile): string {\n  return pipe(\n    profile.user,\n    O.flatMap(u => u.settings),\n    O.map(s => s.theme),\n    O.getOrElse(() => 'light') \u002F\u002F default\n  )\n}\n```\n\n---\n\n## 2. Form Validation with Either\n\nEither is perfect for validation: `Left` = errors, `Right` = valid data.\n\n### Simple Form Validation\n\n```typescript\nimport * as E from 'fp-ts\u002FEither'\nimport * as A from 'fp-ts\u002FArray'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Validation functions return Either\u003CErrorMessage, ValidValue>\nconst validateEmail = (email: string): E.Either\u003Cstring, string> =>\n  email.includes('@')\n    ? E.right(email)\n    : E.left('Invalid email address')\n\nconst validatePassword = (password: string): E.Either\u003Cstring, string> =>\n  password.length >= 8\n    ? E.right(password)\n    : E.left('Password must be at least 8 characters')\n\nconst validateName = (name: string): E.Either\u003Cstring, string> =>\n  name.trim().length > 0\n    ? E.right(name.trim())\n    : E.left('Name is required')\n```\n\n### Collecting All Errors (Not Just First One)\n\n```typescript\nimport * as E from 'fp-ts\u002FEither'\nimport { sequenceS } from 'fp-ts\u002FApply'\nimport { getSemigroup } from 'fp-ts\u002FNonEmptyArray'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F This collects ALL errors, not just the first one\nconst validateAll = sequenceS(E.getApplicativeValidation(getSemigroup\u003Cstring>()))\n\ninterface SignupForm {\n  name: string\n  email: string\n  password: string\n}\n\ninterface ValidatedForm {\n  name: string\n  email: string\n  password: string\n}\n\nfunction validateForm(form: SignupForm): E.Either\u003Cstring[], ValidatedForm> {\n  return pipe(\n    validateAll({\n      name: pipe(validateName(form.name), E.mapLeft(e => [e])),\n      email: pipe(validateEmail(form.email), E.mapLeft(e => [e])),\n      password: pipe(validatePassword(form.password), E.mapLeft(e => [e])),\n    })\n  )\n}\n\n\u002F\u002F Usage in component\nfunction SignupForm() {\n  const [form, setForm] = useState({ name: '', email: '', password: '' })\n  const [errors, setErrors] = useState\u003Cstring[]>([])\n\n  const handleSubmit = () => {\n    pipe(\n      validateForm(form),\n      E.match(\n        (errs) => setErrors(errs),     \u002F\u002F Show all errors\n        (valid) => {\n          setErrors([])\n          submitToServer(valid)         \u002F\u002F Submit valid data\n        }\n      )\n    )\n  }\n\n  return (\n    \u003Cform onSubmit={e => { e.preventDefault(); handleSubmit() }}>\n      \u003Cinput\n        value={form.name}\n        onChange={e => setForm(f => ({ ...f, name: e.target.value }))}\n        placeholder=\"Name\"\n      \u002F>\n      \u003Cinput\n        value={form.email}\n        onChange={e => setForm(f => ({ ...f, email: e.target.value }))}\n        placeholder=\"Email\"\n      \u002F>\n      \u003Cinput\n        type=\"password\"\n        value={form.password}\n        onChange={e => setForm(f => ({ ...f, password: e.target.value }))}\n        placeholder=\"Password\"\n      \u002F>\n\n      {errors.length > 0 && (\n        \u003Cul style={{ color: 'red' }}>\n          {errors.map((err, i) => \u003Cli key={i}>{err}\u003C\u002Fli>)}\n        \u003C\u002Ful>\n      )}\n\n      \u003Cbutton type=\"submit\">Sign Up\u003C\u002Fbutton>\n    \u003C\u002Fform>\n  )\n}\n```\n\n### Field-Level Errors (Better UX)\n\n```typescript\ntype FieldErrors = Partial\u003CRecord\u003Ckeyof SignupForm, string>>\n\nfunction validateFormWithFieldErrors(form: SignupForm): E.Either\u003CFieldErrors, ValidatedForm> {\n  const errors: FieldErrors = {}\n\n  pipe(validateName(form.name), E.mapLeft(e => { errors.name = e }))\n  pipe(validateEmail(form.email), E.mapLeft(e => { errors.email = e }))\n  pipe(validatePassword(form.password), E.mapLeft(e => { errors.password = e }))\n\n  return Object.keys(errors).length > 0\n    ? E.left(errors)\n    : E.right({ name: form.name.trim(), email: form.email, password: form.password })\n}\n\n\u002F\u002F In component\n{errors.email && \u003Cspan className=\"error\">{errors.email}\u003C\u002Fspan>}\n```\n\n---\n\n## 3. Data Fetching with TaskEither\n\nTaskEither = async operation that might fail. Perfect for API calls.\n\n### Basic Fetch Hook\n\n```typescript\nimport { useState, useEffect } from 'react'\nimport * as TE from 'fp-ts\u002FTaskEither'\nimport * as E from 'fp-ts\u002FEither'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Wrap fetch in TaskEither\nconst fetchJson = \u003CT>(url: string): TE.TaskEither\u003CError, T> =>\n  TE.tryCatch(\n    async () => {\n      const res = await fetch(url)\n      if (!res.ok) throw new Error(`HTTP ${res.status}`)\n      return res.json()\n    },\n    (err) => err instanceof Error ? err : new Error(String(err))\n  )\n\n\u002F\u002F Custom hook\nfunction useFetch\u003CT>(url: string) {\n  const [data, setData] = useState\u003CT | null>(null)\n  const [error, setError] = useState\u003CError | null>(null)\n  const [loading, setLoading] = useState(true)\n\n  useEffect(() => {\n    setLoading(true)\n    setError(null)\n\n    pipe(\n      fetchJson\u003CT>(url),\n      TE.match(\n        (err) => {\n          setError(err)\n          setLoading(false)\n        },\n        (result) => {\n          setData(result)\n          setLoading(false)\n        }\n      )\n    )()\n  }, [url])\n\n  return { data, error, loading }\n}\n\n\u002F\u002F Usage\nfunction UserList() {\n  const { data, error, loading } = useFetch\u003CUser[]>('\u002Fapi\u002Fusers')\n\n  if (loading) return \u003Cdiv>Loading...\u003C\u002Fdiv>\n  if (error) return \u003Cdiv>Error: {error.message}\u003C\u002Fdiv>\n  return (\n    \u003Cul>\n      {data?.map(user => \u003Cli key={user.id}>{user.name}\u003C\u002Fli>)}\n    \u003C\u002Ful>\n  )\n}\n```\n\n### Chaining API Calls\n\n```typescript\n\u002F\u002F Fetch user, then fetch their posts\nconst fetchUserWithPosts = (userId: string) => pipe(\n  fetchJson\u003CUser>(`\u002Fapi\u002Fusers\u002F${userId}`),\n  TE.flatMap(user => pipe(\n    fetchJson\u003CPost[]>(`\u002Fapi\u002Fusers\u002F${userId}\u002Fposts`),\n    TE.map(posts => ({ ...user, posts }))\n  ))\n)\n```\n\n### Parallel API Calls\n\n```typescript\nimport { sequenceT } from 'fp-ts\u002FApply'\n\n\u002F\u002F Fetch multiple things at once\nconst fetchDashboardData = () => pipe(\n  sequenceT(TE.ApplyPar)(\n    fetchJson\u003CUser>('\u002Fapi\u002Fuser'),\n    fetchJson\u003CStats>('\u002Fapi\u002Fstats'),\n    fetchJson\u003CNotifications[]>('\u002Fapi\u002Fnotifications')\n  ),\n  TE.map(([user, stats, notifications]) => ({\n    user,\n    stats,\n    notifications\n  }))\n)\n```\n\n---\n\n## 4. RemoteData Pattern (The Right Way to Handle Async State)\n\nStop using `{ data, loading, error }` booleans. Use a proper state machine.\n\n### The Pattern\n\n```typescript\n\u002F\u002F RemoteData has exactly 4 states - no impossible combinations\ntype RemoteData\u003CE, A> =\n  | { _tag: 'NotAsked' }                    \u002F\u002F Haven't started yet\n  | { _tag: 'Loading' }                     \u002F\u002F In progress\n  | { _tag: 'Failure'; error: E }           \u002F\u002F Failed\n  | { _tag: 'Success'; data: A }            \u002F\u002F Got it!\n\n\u002F\u002F Constructors\nconst notAsked = \u003CE, A>(): RemoteData\u003CE, A> => ({ _tag: 'NotAsked' })\nconst loading = \u003CE, A>(): RemoteData\u003CE, A> => ({ _tag: 'Loading' })\nconst failure = \u003CE, A>(error: E): RemoteData\u003CE, A> => ({ _tag: 'Failure', error })\nconst success = \u003CE, A>(data: A): RemoteData\u003CE, A> => ({ _tag: 'Success', data })\n\n\u002F\u002F Pattern match all states\nfunction fold\u003CE, A, R>(\n  rd: RemoteData\u003CE, A>,\n  onNotAsked: () => R,\n  onLoading: () => R,\n  onFailure: (e: E) => R,\n  onSuccess: (a: A) => R\n): R {\n  switch (rd._tag) {\n    case 'NotAsked': return onNotAsked()\n    case 'Loading': return onLoading()\n    case 'Failure': return onFailure(rd.error)\n    case 'Success': return onSuccess(rd.data)\n  }\n}\n```\n\n### Hook with RemoteData\n\n```typescript\nfunction useRemoteData\u003CT>(fetchFn: () => Promise\u003CT>) {\n  const [state, setState] = useState\u003CRemoteData\u003CError, T>>(notAsked())\n\n  const execute = async () => {\n    setState(loading())\n    try {\n      const data = await fetchFn()\n      setState(success(data))\n    } catch (err) {\n      setState(failure(err instanceof Error ? err : new Error(String(err))))\n    }\n  }\n\n  return { state, execute }\n}\n\n\u002F\u002F Usage\nfunction UserProfile({ userId }: { userId: string }) {\n  const { state, execute } = useRemoteData(() =>\n    fetch(`\u002Fapi\u002Fusers\u002F${userId}`).then(r => r.json())\n  )\n\n  useEffect(() => { execute() }, [userId])\n\n  return fold(\n    state,\n    () => \u003Cbutton onClick={execute}>Load User\u003C\u002Fbutton>,\n    () => \u003CSpinner \u002F>,\n    (err) => \u003CErrorMessage message={err.message} onRetry={execute} \u002F>,\n    (user) => \u003CUserCard user={user} \u002F>\n  )\n}\n```\n\n### Why RemoteData Beats Booleans\n\n```typescript\n\u002F\u002F ❌ BAD: Impossible states are possible\ninterface BadState {\n  data: User | null\n  loading: boolean\n  error: Error | null\n}\n\u002F\u002F Can have: { data: user, loading: true, error: someError } - what does that mean?!\n\n\u002F\u002F ✅ GOOD: Only valid states exist\ntype GoodState = RemoteData\u003CError, User>\n\u002F\u002F Can only be: NotAsked | Loading | Failure | Success\n```\n\n---\n\n## 5. Referential Stability (Preventing Re-renders)\n\nfp-ts values like `O.some(1)` create new objects each render. React sees them as \"changed\".\n\n### The Problem\n\n```typescript\n\u002F\u002F ❌ BAD: Creates new Option every render\nfunction BadComponent() {\n  const [value, setValue] = useState(O.some(1))\n\n  useEffect(() => {\n    \u002F\u002F This runs EVERY render because O.some(1) !== O.some(1)\n    console.log('value changed')\n  }, [value])\n}\n```\n\n### Solution 1: useMemo\n\n```typescript\n\u002F\u002F ✅ GOOD: Memoize Option creation\nfunction GoodComponent() {\n  const [rawValue, setRawValue] = useState\u003Cnumber | null>(1)\n\n  const value = useMemo(\n    () => O.fromNullable(rawValue),\n    [rawValue]  \u002F\u002F Only recreate when rawValue changes\n  )\n\n  useEffect(() => {\n    \u002F\u002F Now this only runs when rawValue actually changes\n    console.log('value changed')\n  }, [rawValue])  \u002F\u002F Depend on raw value, not Option\n}\n```\n\n### Solution 2: fp-ts-react-stable-hooks\n\n```bash\nnpm install fp-ts-react-stable-hooks\n```\n\n```typescript\nimport { useStableO, useStableEffect } from 'fp-ts-react-stable-hooks'\nimport * as O from 'fp-ts\u002FOption'\nimport * as Eq from 'fp-ts\u002FEq'\n\nfunction StableComponent() {\n  \u002F\u002F Uses fp-ts equality instead of reference equality\n  const [value, setValue] = useStableO(O.some(1))\n\n  \u002F\u002F Effect that understands Option equality\n  useStableEffect(\n    () => { console.log('value changed') },\n    [value],\n    Eq.tuple(O.getEq(Eq.eqNumber))  \u002F\u002F Custom equality\n  )\n}\n```\n\n---\n\n## 6. Dependency Injection with Context\n\nUse ReaderTaskEither for testable components with injected dependencies.\n\n### Setup Dependencies\n\n```typescript\nimport * as RTE from 'fp-ts\u002FReaderTaskEither'\nimport { pipe } from 'fp-ts\u002Ffunction'\nimport { createContext, useContext, ReactNode } from 'react'\n\n\u002F\u002F Define what services your app needs\ninterface AppDependencies {\n  api: {\n    getUser: (id: string) => Promise\u003CUser>\n    updateUser: (id: string, data: Partial\u003CUser>) => Promise\u003CUser>\n  }\n  analytics: {\n    track: (event: string, data?: object) => void\n  }\n}\n\n\u002F\u002F Create context\nconst DepsContext = createContext\u003CAppDependencies | null>(null)\n\n\u002F\u002F Provider\nfunction AppProvider({ deps, children }: { deps: AppDependencies; children: ReactNode }) {\n  return \u003CDepsContext.Provider value={deps}>{children}\u003C\u002FDepsContext.Provider>\n}\n\n\u002F\u002F Hook to use dependencies\nfunction useDeps(): AppDependencies {\n  const deps = useContext(DepsContext)\n  if (!deps) throw new Error('Missing AppProvider')\n  return deps\n}\n```\n\n### Use in Components\n\n```typescript\nfunction UserProfile({ userId }: { userId: string }) {\n  const { api, analytics } = useDeps()\n  const [user, setUser] = useState\u003CRemoteData\u003CError, User>>(notAsked())\n\n  useEffect(() => {\n    setUser(loading())\n    api.getUser(userId)\n      .then(u => {\n        setUser(success(u))\n        analytics.track('user_viewed', { userId })\n      })\n      .catch(e => setUser(failure(e)))\n  }, [userId, api, analytics])\n\n  \u002F\u002F render...\n}\n```\n\n### Testing with Mock Dependencies\n\n```typescript\nconst mockDeps: AppDependencies = {\n  api: {\n    getUser: jest.fn().mockResolvedValue({ id: '1', name: 'Test User' }),\n    updateUser: jest.fn().mockResolvedValue({ id: '1', name: 'Updated' }),\n  },\n  analytics: {\n    track: jest.fn(),\n  },\n}\n\ntest('loads user on mount', async () => {\n  render(\n    \u003CAppProvider deps={mockDeps}>\n      \u003CUserProfile userId=\"1\" \u002F>\n    \u003C\u002FAppProvider>\n  )\n\n  await screen.findByText('Test User')\n  expect(mockDeps.api.getUser).toHaveBeenCalledWith('1')\n})\n```\n\n---\n\n## 7. React 19 Patterns\n\n### use() for Promises (React 19+)\n\n```typescript\nimport { use, Suspense } from 'react'\n\n\u002F\u002F Instead of useEffect + useState for data fetching\nfunction UserProfile({ userPromise }: { userPromise: Promise\u003CUser> }) {\n  const user = use(userPromise)  \u002F\u002F Suspends until resolved\n  return \u003Cdiv>{user.name}\u003C\u002Fdiv>\n}\n\n\u002F\u002F Parent provides the promise\nfunction App() {\n  const userPromise = fetchUser('1')  \u002F\u002F Start fetching immediately\n\n  return (\n    \u003CSuspense fallback={\u003CSpinner \u002F>}>\n      \u003CUserProfile userPromise={userPromise} \u002F>\n    \u003C\u002FSuspense>\n  )\n}\n```\n\n### useActionState for Forms (React 19+)\n\n```typescript\nimport { useActionState } from 'react'\nimport * as E from 'fp-ts\u002FEither'\n\ninterface FormState {\n  errors: string[]\n  success: boolean\n}\n\nasync function submitForm(\n  prevState: FormState,\n  formData: FormData\n): Promise\u003CFormState> {\n  const data = {\n    email: formData.get('email') as string,\n    password: formData.get('password') as string,\n  }\n\n  \u002F\u002F Use Either for validation\n  const result = pipe(\n    validateForm(data),\n    E.match(\n      (errors) => ({ errors, success: false }),\n      async (valid) => {\n        await saveToServer(valid)\n        return { errors: [], success: true }\n      }\n    )\n  )\n\n  return result\n}\n\nfunction SignupForm() {\n  const [state, formAction, isPending] = useActionState(submitForm, {\n    errors: [],\n    success: false\n  })\n\n  return (\n    \u003Cform action={formAction}>\n      \u003Cinput name=\"email\" type=\"email\" \u002F>\n      \u003Cinput name=\"password\" type=\"password\" \u002F>\n\n      {state.errors.map(e => \u003Cp key={e} className=\"error\">{e}\u003C\u002Fp>)}\n\n      \u003Cbutton disabled={isPending}>\n        {isPending ? 'Submitting...' : 'Sign Up'}\n      \u003C\u002Fbutton>\n    \u003C\u002Fform>\n  )\n}\n```\n\n### useOptimistic for Instant Feedback (React 19+)\n\n```typescript\nimport { useOptimistic } from 'react'\n\nfunction TodoList({ todos }: { todos: Todo[] }) {\n  const [optimisticTodos, addOptimisticTodo] = useOptimistic(\n    todos,\n    (state, newTodo: Todo) => [...state, { ...newTodo, pending: true }]\n  )\n\n  const addTodo = async (text: string) => {\n    const newTodo = { id: crypto.randomUUID(), text, done: false }\n\n    \u002F\u002F Immediately show in UI\n    addOptimisticTodo(newTodo)\n\n    \u002F\u002F Actually save (will reconcile when done)\n    await saveTodo(newTodo)\n  }\n\n  return (\n    \u003Cul>\n      {optimisticTodos.map(todo => (\n        \u003Cli key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>\n          {todo.text}\n        \u003C\u002Fli>\n      ))}\n    \u003C\u002Ful>\n  )\n}\n```\n\n---\n\n## 8. Common Patterns Cheat Sheet\n\n### Render Based on Option\n\n```typescript\n\u002F\u002F Pattern 1: match\npipe(\n  maybeUser,\n  O.match(\n    () => \u003CLoginButton \u002F>,\n    (user) => \u003CUserMenu user={user} \u002F>\n  )\n)\n\n\u002F\u002F Pattern 2: fold (same as match)\nO.fold(\n  () => \u003CLoginButton \u002F>,\n  (user) => \u003CUserMenu user={user} \u002F>\n)(maybeUser)\n\n\u002F\u002F Pattern 3: getOrElse for simple defaults\nconst name = pipe(\n  maybeUser,\n  O.map(u => u.name),\n  O.getOrElse(() => 'Guest')\n)\n```\n\n### Render Based on Either\n\n```typescript\npipe(\n  validationResult,\n  E.match(\n    (errors) => \u003CErrorList errors={errors} \u002F>,\n    (data) => \u003CSuccessMessage data={data} \u002F>\n  )\n)\n```\n\n### Safe Array Rendering\n\n```typescript\nimport * as A from 'fp-ts\u002FArray'\n\n\u002F\u002F Get first item safely\nconst firstUser = pipe(\n  users,\n  A.head,\n  O.map(user => \u003CFeatured user={user} \u002F>),\n  O.getOrElse(() => \u003CNoFeaturedUser \u002F>)\n)\n\n\u002F\u002F Find specific item\nconst adminUser = pipe(\n  users,\n  A.findFirst(u => u.role === 'admin'),\n  O.map(admin => \u003CAdminBadge user={admin} \u002F>),\n  O.toNullable  \u002F\u002F or O.getOrElse(() => null)\n)\n```\n\n### Conditional Props\n\n```typescript\n\u002F\u002F Add props only if value exists\nconst modalProps = {\n  isOpen: true,\n  ...pipe(\n    maybeTitle,\n    O.map(title => ({ title })),\n    O.getOrElse(() => ({}))\n  )\n}\n```\n\n---\n\n## When to Use What\n\n| Situation | Use |\n|-----------|-----|\n| Value might not exist | `Option\u003CT>` |\n| Operation might fail (sync) | `Either\u003CE, A>` |\n| Async operation might fail | `TaskEither\u003CE, A>` |\n| Need loading\u002Ferror\u002Fsuccess UI | `RemoteData\u003CE, A>` |\n| Form with multiple validations | `Either` with validation applicative |\n| Dependency injection | Context + `ReaderTaskEither` |\n| Prevent re-renders with fp-ts | `useMemo` or `fp-ts-react-stable-hooks` |\n\n---\n\n## Libraries\n\n- **[fp-ts](https:\u002F\u002Fgithub.com\u002Fgcanti\u002Ffp-ts)** - Core library\n- **[fp-ts-react-stable-hooks](https:\u002F\u002Fgithub.com\u002Fmblink\u002Ffp-ts-react-stable-hooks)** - Stable hooks\n- **[@devexperts\u002Fremote-data-ts](https:\u002F\u002Fgithub.com\u002Fdevexperts\u002Fremote-data-ts)** - RemoteData\n- **[io-ts](https:\u002F\u002Fgithub.com\u002Fgcanti\u002Fio-ts)** - Runtime type validation\n- **[zod](https:\u002F\u002Fgithub.com\u002Fcolinhacks\u002Fzod)** - Schema validation (works great with fp-ts)\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.\n","","imported","https:\u002F\u002Fgithub.com\u002Fsickn33\u002Fantigravity-awesome-skills","user_system_seed","SkillOPIC",true,74,1454,"2026-05-16 13:18:48",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"编程开发","coding","mdi-code-braces","代码生成、调试、审查，提升开发效率",2,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":32,"skillCount":33,"createdAt":26},"前端开发","frontend","mdi-language-html5","HTML\u002FCSS\u002FJavaScript\u002F框架相关",1,96,[35],{"id":36,"skillId":4,"version":37,"fileName":38,"fileSize":39,"filePath":40,"fileHash":41,"manifest":42,"createdAt":19},"a2350656-8cad-4186-b99c-9b45fb82423e","1.0.0","fp-react.zip",6474,"uploads\u002Fskills\u002F6cbbead3-7da0-438d-981f-9f64ec1b37fa\u002Ffp-react.zip","489183821cad67667e72fd38a8d4933ba02a1b6126d67962a0711585430354a3","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":18908}]",{"code":44,"message":45,"data":46},200,"success",{"items":47,"stats":48,"page":51},[],{"averageRating":49,"totalRatings":49,"ratingCounts":50},0,[49,49,49,49,49],{"limit":52,"offset":49,"hasMore":53,"nextOffset":52,"ratedOnly":16},15,false]