fp-ts-react

Polar Sponsor
爱发电 赞助
.NET 9.0

在 React 中使用 fp-ts 的实用模式——hooks、状态、表单、数据获取。在构建使用函数式编程模式的 React 应用时使用。兼容 React 18/

在 React 中运用函数式编程

功能概述

在 React 中运用函数式编程是一项面向实际任务的技能,主要用于针对 React 应用的实用模式;没有黑话,只有能用的代码;

核心要点

  • 何时使用本技能;
  • 使用 fp-ts 为 React 应用做类型安全的状态管理;
  • 在数据获取中处理 loading/error/success 状态;

使用与执行

实现带错误累积的表单校验;使用 React 18/19 或 Next.js 14/15 配合函数式模式;Option;值可能缺失(用户尚未加。它将相关步骤、工具调用和结果整理方式集中到统一流程中,帮助使用者更快完成目标并减少重复操作。从功能定位来看,该技能强调把分散的操作要求整理成清晰、可复用的处理流程,使用户能够围绕既定目标快速准备输入、选择执行方式并获得结构化结果。

结果检查与注意事项

实际使用前应先确认任务范围、数据来源、运行环境、必要权限和关键参数,再依据技能说明逐步执行;若输入条件不完整,应先补齐信息或采用保守配置,避免因错误假设导致结果偏离需求。执行过程中需要关注工具调用是否成功、接口或依赖是否可用、输出格式是否符合预期,并对异常提示、缺失字段和边界情况进行处理;涉及批量任务时,还应保存进度,避免中断后重复操作。该技能适合用于一次性任务,也可以接入自动化工作流,与其他技能或上层代理配合完成更完整的业务链路;在组合使用时,应明确每一步的输入输出关系,并避免不同步骤之间出现参数冲突。

在 React 中运用函数式编程

针对 React 应用的实用模式。没有黑话,只有能用的代码。

何时使用本技能

  • 使用 fp-ts 为 React 应用做类型安全的状态管理
  • 在数据获取中处理 loading/error/success 状态
  • 实现带错误累积的表单校验
  • 使用 React 18/19 或 Next.js 14/15 配合函数式模式

快速参考

模式 使用场景
Option 值可能缺失(用户尚未加载)
Either 操作可能失败(表单校验)
TaskEither 异步操作可能失败(API 调用)
RemoteData 需要展示 loading/error/success 状态
pipe 链式调用多个变换

1. 用 Option 管理状态(可能有点,也可能没有)

使用 Option 而非 null | undefined,以表达更清晰的意图。

基础模式

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 表示"这个可能还不存在"
  const [user, setUser] = useState>(O.none)

  const handleLogin = (userData: User) => {
    setUser(O.some(userData))
  }

  const handleLogout = () => {
    setUser(O.none)
  }

  return pipe(
    user,
    O.match(
      // 当没有用户时
      () => ,
      // 当有用户时
      (u) => (
        

Welcome, {u.name}!

) ) ) }

链式调用可选值

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') // 默认值
  )
}

2. 用 Either 做表单校验

Either 非常适合校验:Left = 错误,Right = 合法数据。

简单表单校验

import * as E from 'fp-ts/Either'
import * as A from 'fp-ts/Array'
import { pipe } from 'fp-ts/function'

// 校验函数返回 Either
const validateEmail = (email: string): E.Either =>
  email.includes('@')
    ? E.right(email)
    : E.left('Invalid email address')

const validatePassword = (password: string): E.Either =>
  password.length >= 8
    ? E.right(password)
    : E.left('Password must be at least 8 characters')

const validateName = (name: string): E.Either =>
  name.trim().length > 0
    ? E.right(name.trim())
    : E.left('Name is required')

收集全部错误(而非仅第一个)

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'

// 这会收集全部错误,而非仅第一个
const validateAll = sequenceS(E.getApplicativeValidation(getSemigroup()))

interface SignupForm {
  name: string
  email: string
  password: string
}

interface ValidatedForm {
  name: string
  email: string
  password: string
}

function validateForm(form: SignupForm): E.Either {
  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])),
    })
  )
}

// 在组件中使用
function SignupForm() {
  const [form, setForm] = useState({ name: '', email: '', password: '' })
  const [errors, setErrors] = useState([])

  const handleSubmit = () => {
    pipe(
      validateForm(form),
      E.match(
        (errs) => setErrors(errs),     // 展示所有错误
        (valid) => {
          setErrors([])
          submitToServer(valid)         // 提交合法数据
        }
      )
    )
  }

  return (
    
{ e.preventDefault(); handleSubmit() }}> setForm(f => ({ ...f, name: e.target.value }))} placeholder="Name" /> setForm(f => ({ ...f, email: e.target.value }))} placeholder="Email" /> setForm(f => ({ ...f, password: e.target.value }))} placeholder="Password" /> {errors.length > 0 && (
    {errors.map((err, i) =>
  • {err}
  • )}
)}
) }

字段级错误(更好的 UX)

type FieldErrors = Partial>

function validateFormWithFieldErrors(form: SignupForm): E.Either {
  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 })
}

// 在组件中
{errors.email && {errors.email}}

3. 用 TaskEither 获取数据

TaskEither = 可能失败的异步操作。非常适合 API 调用。

基础获取 Hook

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'

// 将 fetch 包裹进 TaskEither
const fetchJson = (url: string): TE.TaskEither =>
  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))
  )

// 自定义 hook
function useFetch(url: string) {
  const [data, setData] = useState(null)
  const [error, setError] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    setLoading(true)
    setError(null)

    pipe(
      fetchJson(url),
      TE.match(
        (err) => {
          setError(err)
          setLoading(false)
        },
        (result) => {
          setData(result)
          setLoading(false)
        }
      )
    )()
  }, [url])

  return { data, error, loading }
}

// 使用
function UserList() {
  const { data, error, loading } = useFetch('/api/users')

  if (loading) return 
Loading...
if (error) return
Error: {error.message}
return (
    {data?.map(user =>
  • {user.name}
  • )}
) }

链式调用 API

// 获取用户,再获取其帖子
const fetchUserWithPosts = (userId: string) => pipe(
  fetchJson(`/api/users/${userId}`),
  TE.flatMap(user => pipe(
    fetchJson(`/api/users/${userId}/posts`),
    TE.map(posts => ({ ...user, posts }))
  ))
)

并行 API 调用

import { sequenceT } from 'fp-ts/Apply'

// 一次性获取多个数据
const fetchDashboardData = () => pipe(
  sequenceT(TE.ApplicativePar)(
    fetchJson('/api/user'),
    fetchJson('/api/stats'),
    fetchJson('/api/notifications')
  ),
  TE.map(([user, stats, notifications]) => ({
    user,
    stats,
    notifications
  }))
)

4. RemoteData 模式(处理异步状态的正确方式)

别再使用 { data, loading, error } 布尔值了。使用一个恰当的状态机。

模式

// RemoteData 有且仅有 4 种状态 —— 不可能出现非法组合
type RemoteData =
  | { _tag: 'NotAsked' }                    // 还没开始
  | { _tag: 'Loading' }                     // 进行中
  | { _tag: 'Failure'; error: E }           // 失败了
  | { _tag: 'Success'; data: A }            // 拿到了!

// 构造函数
const notAsked = (): RemoteData => ({ _tag: 'NotAsked' })
const loading = (): RemoteData => ({ _tag: 'Loading' })
const failure = (error: E): RemoteData => ({ _tag: 'Failure', error })
const success = (data: A): RemoteData => ({ _tag: 'Success', data })

// 对所有状态进行模式匹配
function fold(
  rd: RemoteData,
  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)
  }
}

配合 RemoteData 的 Hook

function useRemoteData(fetchFn: () => Promise) {
  const [state, setState] = useState>(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 }
}

// 使用
function UserProfile({ userId }: { userId: string }) {
  const { state, execute } = useRemoteData(() =>
    fetch(`/api/users/${userId}`).then(r => r.json())
  )

  useEffect(() => { execute() }, [userId])

  return fold(
    state,
    () => ,
    () => ,
    (err) => ,
    (user) => 
  )
}

为什么 RemoteData 优于布尔值

// ❌ 差:可能产生不可能的状态
interface BadState {
  data: User | null
  loading: boolean
  error: Error | null
}
// 可能出现:{ data: user, loading: true, error: someError } —— 这算什么?!

// ✅ 好:只存在合法状态
type GoodState = RemoteData
// 只能是:NotAsked | Loading | Failure | Success

5. 引用稳定性(防止重复渲染)

像 O.some(1) 这样的 fp-ts 值会在每次渲染时创建新对象。React 会认为它们"变了"。

问题

// ❌ 差:每次渲染都创建新 Option
function BadComponent() {
  const [value, setValue] = useState(O.some(1))

  useEffect(() => {
    // 这个会每次渲染都运行,因为 O.some(1) !== O.some(1)
    console.log('value changed')
  }, [value])
}

方案 1:useMemo

// ✅ 好:把 Option 的创建记忆化
function GoodComponent() {
  const [rawValue, setRawValue] = useState(1)

  const value = useMemo(
    () => O.fromNullable(rawValue),
    [rawValue]  // 只有 rawValue 变化时才重建
  )

  useEffect(() => {
    // 现在这个只在 rawValue 真正变化时才运行
    console.log('value changed')
  }, [rawValue])  // 依赖原始值,而非 Option
}

方案 2:fp-ts-react-stable-hooks

npm install fp-ts-react-stable-hooks
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() {
  // 使用 fp-ts 的等值判断,而非引用等值
  const [value, setValue] = useStableO(O.some(1))

  // 能理解 Option 等值的 effect
  useStableEffect(
    () => { console.log('value changed') },
    [value],
    Eq.tuple(O.getEq(Eq.eqNumber))  // 自定义等值
  )
}

6. 用 Context 做依赖注入

使用 ReaderTaskEither 来构建可测试的、依赖被注入的组件。

配置依赖

import * as RTE from 'fp-ts/ReaderTaskEither'
import { pipe } from 'fp-ts/function'
import { createContext, useContext, ReactNode } from 'react'

// 定义你的应用需要哪些服务
interface AppDependencies {
  api: {
    getUser: (id: string) => Promise
    updateUser: (id: string, data: Partial) => Promise
  }
  analytics: {
    track: (event: string, data?: object) => void
  }
}

// 创建 context
const DepsContext = createContext(null)

// Provider
function AppProvider({ deps, children }: { deps: AppDependencies; children: ReactNode }) {
  return {children}
}

// 使用依赖的 hook
function useDeps(): AppDependencies {
  const deps = useContext(DepsContext)
  if (!deps) throw new Error('Missing AppProvider')
  return deps
}

在组件中使用

function UserProfile({ userId }: { userId: string }) {
  const { api, analytics } = useDeps()
  const [user, setUser] = useState>(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])

  // 渲染……
}

用模拟依赖做测试

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(
    
      
    
  )

  await screen.findByText('Test User')
  expect(mockDeps.api.getUser).toHaveBeenCalledWith('1')
})

7. React 19 模式

用 use() 处理 Promise(React 19+)

import { use, Suspense } from 'react'

// 不再用 useEffect + useState 来获取数据
function UserProfile({ userPromise }: { userPromise: Promise }) {
  const user = use(userPromise)  // 在 resolve 前挂起
  return 
{user.name}
} // 父组件提供 promise function App() { const userPromise = fetchUser('1') // 立即开始获取 return ( }> ) }

用 useActionState 处理表单(React 19+)

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 {
  const data = {
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  }

  // 使用 Either 做校验
  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 (
    
{state.errors.map(e =>

{e}

)}
) }

用 useOptimistic 实现即时反馈(React 19+)

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 }

    // 立即在 UI 中展示
    addOptimisticTodo(newTodo)

    // 真正保存(完成后会协调)
    await saveTodo(newTodo)
  }

  return (
    
    {optimisticTodos.map(todo => (
  • {todo.text}
  • ))}
) }

8. 常见模式速查表

基于 Option 渲染

// 模式 1:match
pipe(
  maybeUser,
  O.match(
    () => ,
    (user) => 
  )
)

// 模式 2:fold(与 match 相同)
O.fold(
  () => ,
  (user) => 
)(maybeUser)

// 模式 3:用 getOrElse 处理简单默认值
const name = pipe(
  maybeUser,
  O.map(u => u.name),
  O.getOrElse(() => 'Guest')
)

基于 Either 渲染

pipe(
  validationResult,
  E.match(
    (errors) => ,
    (data) => 
  )
)

安全的数组渲染

import * as A from 'fp-ts/Array'

// 安全地获取第一项
const firstUser = pipe(
  users,
  A.head,
  O.map(user => ),
  O.getOrElse(() => )
)

// 查找特定项
const adminUser = pipe(
  users,
  A.findFirst(u => u.role === 'admin'),
  O.map(admin => ),
  O.toNullable  // 或 O.getOrElse(() => null)
)

条件式 props

// 仅当值存在时才添加 props
const modalProps = {
  isOpen: true,
  ...pipe(
    maybeTitle,
    O.map(title => ({ title })),
    O.getOrElse(() => ({}))
  )
}

何时使用什么

场景 使用
值可能不存在 Option
操作可能失败(同步) Either
异步操作可能失败 TaskEither
需要 loading/error/success UI RemoteData
带多重校验的表单 配合 validation applicative 的 Either
依赖注入 Context + ReaderTaskEither
用 fp-ts 防止重复渲染 useMemo 或 fp-ts-react-stable-hooks

相关库

  • fp-ts - 核心库
  • fp-ts-react-stable-hooks - 稳定的 hooks
  • @devexperts/remote-data-ts - RemoteData
  • io-ts - 运行时类型校验
  • zod - Schema 校验(与 fp-ts 配合良好)

相关专题

更多
JavaScript React组件优化与性能提升实战
JavaScript React组件优化与性能提升实战

本专题围绕 React 框架在大型前端项目中的性能优化展开,讲解组件生命周期优化、虚拟 DOM 原理、状态管理策略及懒加载技术。通过实战案例,帮助开发者构建性能优良、响应迅速的前端应用。

2026.04.13

165

18

React 18状态管理与Hooks高级实践
React 18状态管理与Hooks高级实践

本专题专注于 React 18 的高级开发技术,详细讲解 useState、useEffect、useReducer、useContext 等 Hooks 的使用技巧,以及 Redux、Zustand 等状态管理工具的集成与优化方法。通过真实案例,帮助前端开发者构建可维护、性能优良的现代 React 应用。

2026.02.10

183

17

Go-Zero API开发使用教程
Go-Zero API开发使用教程

本专题聚焦Go-Zero的API开发,拆解.api描述文件的语法结构、路由与分组定义、参数绑定与校验规则,说明统一响应体、JWT鉴权、跨域与接口文档生成的落地方式,并给出网关聚合与接口测试的实践建议。

2026.11.12

0

15

Go-Zero安装与goctl环境配置教程
Go-Zero安装与goctl环境配置教程

本专题围绕Go-Zero的安装与工具链配置展开,讲清Go版本要求、goctl安装方式、代理镜像、protoc依赖与环境变量设置,说明多版本共存、升级回退与CI流水线安装的做法,并整理安装阶段的常见报错与验证步骤。

2026.11.12

0

15

Go-Zero零基础入门教程
Go-Zero零基础入门教程

本专题整理Go-Zero的入门知识,从框架定位、环境准备到跑通第一个API服务,讲清目录分层、配置加载、日志与中间件的基本用法,并对比Gin、Kratos的差异,帮助Go开发者建立整体认知,判断它是否适合当前项目。

2026.11.12

0

15

Postman身份认证怎么配置
Postman身份认证怎么配置

本专题从实际API接口调试场景出发,讲解Postman身份认证的完整配置流程,包括常见认证协议介绍、授权参数设置、访问令牌填写、OAuth 2.0授权及认证失败排查,帮助用户正确完成接口授权与访问验证。

2026.11.06

0

12

Postman请求头Headers怎么设置
Postman请求头Headers怎么设置

围绕Postman HTTP请求头配置展开,讲解Headers选项的具体使用方式,包括Key和Value参数填写、请求头信息添加、常见Header字段设置及请求头修改方法,帮助用户解决接口调试过程中遇到的请求头配置问题。

2026.11.06

0

13

Postman接口请求发送教程
Postman接口请求发送教程

详细介绍Postman接口请求的创建与发送方法,涵盖GET和POST请求、URL配置、请求参数、Headers请求头、Body请求体设置以及响应结果查看,帮助用户掌握Postman接口请求的完整操作流程。

2026.11.06

0

10

DBeaver怎么导入数据
DBeaver怎么导入数据

DBeaver数据导入专题,整理Excel、CSV、SQL、TXT等常见文件的数据导入方法,涵盖目标数据库选择、数据表创建、字段映射、数据类型设置及导入参数配置等操作,帮助用户将本地文件中的数据写入数据库,并了解批量导入、重复数据处理和导入失败等常见问题的解决方法。

2026.11.05

0

12

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 12.5万人学习

国外Web开发全栈课程全集
国外Web开发全栈课程全集

共12课时 | 1.4万人学习