在 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 (
)
}
字段级错误(更好的 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 (
)
}
用 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 配合良好)
热门AI工具
相关专题
本专题围绕 React 框架在大型前端项目中的性能优化展开,讲解组件生命周期优化、虚拟 DOM 原理、状态管理策略及懒加载技术。通过实战案例,帮助开发者构建性能优良、响应迅速的前端应用。
165
2026.04.13
本专题专注于 React 18 的高级开发技术,详细讲解 useState、useEffect、useReducer、useContext 等 Hooks 的使用技巧,以及 Redux、Zustand 等状态管理工具的集成与优化方法。通过真实案例,帮助前端开发者构建可维护、性能优良的现代 React 应用。
183
2026.02.10
本专题聚焦Go-Zero的API开发,拆解.api描述文件的语法结构、路由与分组定义、参数绑定与校验规则,说明统一响应体、JWT鉴权、跨域与接口文档生成的落地方式,并给出网关聚合与接口测试的实践建议。
0
2026.11.12
本专题围绕Go-Zero的安装与工具链配置展开,讲清Go版本要求、goctl安装方式、代理镜像、protoc依赖与环境变量设置,说明多版本共存、升级回退与CI流水线安装的做法,并整理安装阶段的常见报错与验证步骤。
0
2026.11.12
本专题整理Go-Zero的入门知识,从框架定位、环境准备到跑通第一个API服务,讲清目录分层、配置加载、日志与中间件的基本用法,并对比Gin、Kratos的差异,帮助Go开发者建立整体认知,判断它是否适合当前项目。
0
2026.11.12
本专题从实际API接口调试场景出发,讲解Postman身份认证的完整配置流程,包括常见认证协议介绍、授权参数设置、访问令牌填写、OAuth 2.0授权及认证失败排查,帮助用户正确完成接口授权与访问验证。
0
2026.11.06
围绕Postman HTTP请求头配置展开,讲解Headers选项的具体使用方式,包括Key和Value参数填写、请求头信息添加、常见Header字段设置及请求头修改方法,帮助用户解决接口调试过程中遇到的请求头配置问题。
0
2026.11.06
详细介绍Postman接口请求的创建与发送方法,涵盖GET和POST请求、URL配置、请求参数、Headers请求头、Body请求体设置以及响应结果查看,帮助用户掌握Postman接口请求的完整操作流程。
0
2026.11.06
DBeaver数据导入专题,整理Excel、CSV、SQL、TXT等常见文件的数据导入方法,涵盖目标数据库选择、数据表创建、字段映射、数据类型设置及导入参数配置等操作,帮助用户将本地文件中的数据写入数据库,并了解批量导入、重复数据处理和导入失败等常见问题的解决方法。
0
2026.11.05
热门下载
相关下载
精品课程