name: coding-standards description: “React 19 和 TypeScript 编码规范,适用于 Portfolio Buddy 2。使用场景:编写新组件、代码审查、重构或确保一致性。包含组件模式、TypeScript 规则和最佳实践。”
编码规范 - Portfolio Buddy 2
React 19 模式
组件结构
// 良好:使用 TypeScript 的函数式组件
interface MetricsTableProps {
data: Metric[]
onSelect: (id: string) => void
}
export function MetricsTable({ data, onSelect }: MetricsTableProps) {
// Hooks 放在顶部
const [selected, setSelected] = useState<Set<string>>(new Set())
// 使用 useMemo 处理派生状态
const sortedData = useMemo(() =>
data.sort((a, b) => b.sharpe - a.sharpe),
[data]
)
// 使用 useCallback 处理事件处理器
const handleSelect = useCallback((id: string) => {
setSelected(prev => new Set(prev).add(id))
onSelect(id)
}, [onSelect])
// 渲染
return <div>...</div>
}
Hooks 规则
- 仅在顶层使用 - 不要在条件语句或循环中使用 hooks
- 自定义 hooks 以
use开头 - useMetrics, usePortfolio, useSorting - 依赖数组完整 - useEffect/useMemo/useCallback 中的所有依赖项
- 卸载时清理 - 从 useEffect 返回清理函数
状态管理
Portfolio Buddy 2 仅使用纯 React Hooks:
- 本地 UI 状态 →
useState - 派生状态 →
useMemo - 稳定的回调函数 →
useCallback - DOM/值引用 →
useRef
不使用全局状态库:
- ❌ 不使用 TanStack Query
- ❌ 不使用 Zustand
- ❌ 不使用 Redux
- ❌ 不使用 Jotai
模式:属性向下传递,自定义 hooks 用于共享逻辑
// 状态管理示例
const [files, setFiles] = useState<File[]>([])
const [dateRange, setDateRange] = useState({ start: null, end: null })
// 派生状态
const filteredData = useMemo(() =>
filterByDateRange(files, dateRange),
[files, dateRange]
)
// 稳定的回调
const handleUpload = useCallback((newFile: File) => {
setFiles(prev => [...prev, newFile])
}, [])
TypeScript 标准
不使用 any 类型
// 错误
const data: any = fetchData()
// 正确
interface TradeData {
symbol: string
date: Date
pnl: number
}
const data: TradeData[] = fetchData()
当前违规(技术债务):
- usePortfolio.ts: 11 处实例(交易/指标类型)
- useMetrics.ts: 4 处实例(排序比较)
- dataUtils.ts: 1 处实例(Metrics 接口)
- 总计:15 处违规待修复(原为 16 处)
严格的空值检查
// 错误
const value = data.find(x => x.id === id)
value.name // 可能为 undefined!
// 正确
const value = data.find(x => x.id === id)
if (value) {
value.name // 类型安全
}
// 或使用可选链
const name = data.find(x => x.id === id)?.name
在明显时使用类型推断
// 冗余
const count: number = 5
const name: string = 'Portfolio Buddy'
// 更好(TypeScript 推断)
const count = 5
const name = 'Portfolio Buddy'
// 需要时显式声明
const metrics: Metric[] = [] // 空数组需要类型
组件大小限制
每个组件最多 200 行
当组件超过 200 行时:
- 提取子组件
- 将逻辑移至自定义 hooks
- 提取工具函数到 utils/
当前违规
⚠️ 必须重构:
- PortfolioSection.tsx: 591 行(限制的 295%)
- 提取 EquityChartSection
- 提取 PortfolioStats
- 提取 ContractControls
- 仅保留编排逻辑
应该重构:
- App.tsx: 351 行(限制的 175%)
- 将各部分提取为组件
- MetricsTable.tsx: 242 行(限制的 121%)
- 已从 350 行改进,但仍超限
重构示例
// 之前:PortfolioSection 中的 591 行
function PortfolioSection() {
// 合约乘数逻辑(50 行)
// 日期过滤逻辑(40 行)
// 图表配置(100 行)
// 统计计算(80 行)
// 渲染逻辑(300+ 行)
}
// 之后:拆分为专注的部分
function PortfolioSection() {
const portfolio = usePortfolio(files, dateRange)
const contracts = useContractMultipliers(portfolio.strategies)
return (
<div>
<ContractControls {...contracts} />
<EquityChartSection data={portfolio.equity} />
<PortfolioStats metrics={portfolio.metrics} />
</div>
)
}
文件组织
实际目录结构
src/
├── components/
│ └── [AllComponents].tsx (扁平结构,无子目录)
├── hooks/
│ ├── useContractMultipliers.ts
│ ├── useMetrics.ts
│ ├── usePortfolio.ts
│ └── useSorting.ts
├── utils/
│ └── dataUtils.ts (指标计算、解析)
├── App.tsx
└── main.tsx
注意:没有 ui/ 或 charts/ 子目录 - 组件扁平化放在 components/ 中
命名约定
- 组件:帕斯卡命名法 -
MetricsTable.tsx,CorrelationHeatmap.tsx - Hooks:驼峰命名法,以
use开头 -useMetrics.ts,useSorting.ts - 工具函数:驼峰命名法 -
calculateMetrics(),parseCSV() - 类型/接口:帕斯卡命名法 -
interface Metric,type Trade
错误处理
始终处理错误
// 错误
const data = await supabase.storage.upload(file)
// 正确
const { data, error } = await supabase.storage.upload(file)
if (error) {
console.error('上传失败:', error)
toast.error('文件上传失败')
return
}
使用 Try-Catch 进行解析
// 带错误处理的 CSV 解析
try {
const parsed = parseCSV(file)
setData(parsed.data)
if (parsed.errors.length > 0) {
setErrors(parsed.errors)
}
} catch (error) {
console.error('解析错误:', error)
toast.error('CSV 格式无效')
}
错误边界
当前状态:未实现错误边界(技术债务)
应该添加:
<ErrorBoundary fallback={<ErrorMessage />}>
<PortfolioSection />
</ErrorBoundary>
性能
记忆化
// 昂贵的计算
const metrics = useMemo(
() => calculateMetrics(portfolioData, riskFreeRate),
[portfolioData, riskFreeRate]
)
// 大型数据转换
const correlationMatrix = useMemo(
() => buildCorrelationMatrix(selectedStrategies),
[selectedStrategies]
)
回调稳定性
// 防止子组件重新渲染
const handleSort = useCallback((column: string) => {
setSortColumn(column)
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc')
}, [])
// 将稳定的回调传递给子组件
<SortableHeader onSort={handleSort} />
避免过早优化
- 先构建功能
- 如果出现问题则测量性能
- 基于性能分析数据进行优化
- 没有证据时不进行优化
Chart.js 集成
图表组件模式
import { Line } from 'react-chartjs-2'
import { Chart as ChartJS, registerables } from 'chart.js'
import zoomPlugin from 'chartjs-plugin-zoom'
// 一次性注册插件
ChartJS.register(...registerables, zoomPlugin)
function EquityChart({ data }: { data: EquityData[] }) {
const chartData = useMemo(() => ({
labels: data.map(d => d.date),
datasets: [{
label: '权益',
data: data.map(d => d.value),
borderColor: 'rgb(75, 192, 192)',
}]
}), [data])
const options = useMemo(() => ({
responsive: true,
plugins: {
zoom: { enabled: true }
}
}), [])
return <Line data={chartData} options={options} />
}
图表库
- ✅ 使用:Chart.js + react-chartjs-2
- ❌ 不使用:Recharts(已安装但未使用,应移除)
测试标准
测试内容
- ✅ 关键计算(夏普比率、索提诺比率、相关性)
- ✅ 数据转换(CSV 解析、指标计算)
- ✅ 错误状态和边界情况
- ✅ Hook 返回值
- ❌ UI 实现细节(className、DOM 结构)
- ❌ 第三方库内部实现
测试结构
describe('calculateMetrics', () => {
it('正确计算夏普比率', () => {
const trades = mockTradeData()
const result = calculateMetrics(trades, 0.02)
expect(result.sharpe).toBeCloseTo(1.5, 2)
})
it('优雅处理空数据', () => {
const result = calculateMetrics([], 0.02)
expect(result.sharpe).toBe(0)
})
})
当前状态:未实现测试(未来工作)
导入组织
导入顺序
// 1. React 和外部库
import { useState, useMemo, useCallback } from 'react'
import { Line } from 'react-chartjs-2'
// 2. 内部 hooks
import { useMetrics } from '@/hooks/useMetrics'
import { usePortfolio } from '@/hooks/usePortfolio'
// 3. 工具函数和辅助函数
import { calculateMetrics, formatCurrency } from '@/utils/dataUtils'
// 4. 类型
import type { Metric, Trade } from '@/types'
// 5. 样式(如果有)
import './styles.css'
代码注释
何时注释
// 良好:解释为什么,而不是做什么
// 通过乘以 sqrt(252) 个交易日进行年化
const sharpe = (avgReturn / stdDev) * Math.sqrt(252)
// 错误:代码作用显而易见
// 计算夏普比率
const sharpe = (avgReturn / stdDev) * Math.sqrt(252)
复杂函数的 JSDoc
/**
* 使用下行偏差计算索提诺比率
* @param returns - 日收益率数组
* @param riskFreeRate - 年化无风险利率(例如,0.02 表示 2%)
* @param targetReturn - 目标收益率阈值(默认:0)
* @returns 年化索提诺比率
*/
function calculateSortino(
returns: number[],
riskFreeRate: number,
targetReturn = 0
): number {
// 实现
}
Git 提交消息
格式
<类型>: <主题>
<正文>
类型
feat:新功能fix:错误修复refactor:代码重构perf:性能改进docs:文档test:测试添加/更改
最近提交的示例
通过年化下行偏差和修正方差计算来修复索提诺比率计算
重构投资组合计算并增强 Supabase 客户端验证;添加无风险利率输入和索提诺比率计算
增强 Supabase 数据获取中的错误处理和验证;更新 MetricsTable 和 PortfolioSection 以管理 selectedTradeLists 状态
代码审查清单
提交代码前:
- [ ] TypeScript 严格模式通过(除非记录为技术债务,否则不使用
any) - [ ] 组件少于 200 行(或有重构计划)
- [ ] 错误处理到位
- [ ] 昂贵计算使用记忆化
- [ ] 使用 useCallback 的稳定回调
- [ ] 正确的 TypeScript 类型(不使用
any) - [ ] 按类别组织的导入
- [ ] 复杂函数有 JSDoc
- [ ] 已移除 console.logs
- [ ] 使用 Chart.js(而非 Recharts)