studio-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Studio Best Practices

Studio最佳实践

Applies to
apps/studio/**/*.{ts,tsx}
.
适用于
apps/studio/**/*.{ts,tsx}
文件。

Boolean Naming

布尔值命名

Use descriptive prefixes — derive from existing state rather than storing separately:
  • is
    — state/identity:
    isLoading
    ,
    isPaused
    ,
    isNewRecord
  • has
    — possession:
    hasPermission
    ,
    hasData
  • can
    — capability:
    canUpdateColumns
    ,
    canDelete
  • should
    — conditional behavior:
    shouldFetch
    ,
    shouldRender
Extract complex conditions into named variables:
tsx
// ❌ inline multi-condition
{
  !isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading && <Button />
}

// ✅ named variable
const canShowAddButton =
  !isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading
{
  canShowAddButton && <Button />
}
Derive booleans — don't store them:
tsx
// ❌ stored derived state
const [isFormValid, setIsFormValid] = useState(false)
useEffect(() => {
  setIsFormValid(name.length > 0 && email.includes('@'))
}, [name, email])

// ✅ derived
const isFormValid = name.length > 0 && email.includes('@')
使用具有描述性的前缀,从现有状态派生而不是单独存储:
  • is
    表示状态/身份:
    isLoading
    isPaused
    isNewRecord
  • has
    表示持有属性:
    hasPermission
    hasData
  • can
    表示具备能力:
    canUpdateColumns
    canDelete
  • should
    表示条件行为:
    shouldFetch
    shouldRender
将复杂条件提取为命名变量:
tsx
// ❌ 行内多条件判断
{
  !isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading && <Button />
}

// ✅ 命名变量
const canShowAddButton =
  !isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading
{
  canShowAddButton && <Button />
}
派生布尔值,不要主动存储:
tsx
// ❌ 存储派生状态
const [isFormValid, setIsFormValid] = useState(false)
useEffect(() => {
  setIsFormValid(name.length > 0 && email.includes('@'))
}, [name, email])

// ✅ 直接派生
const isFormValid = name.length > 0 && email.includes('@')

Component Structure

组件结构

See
vercel-composition-patterns
skill for compound component and composition patterns.
Keep components under 200–300 lines. Split when you see:
  • Multiple distinct UI sections
  • Complex conditional rendering
  • Multiple unrelated
    useState
    calls
  • Hard to understand at a glance
Co-locate sub-components in the same directory as the parent. Avoid barrel re-export files.
Extract repeated JSX patterns into small components.
参考
vercel-composition-patterns
skill 了解复合组件和组合模式。
组件代码长度保持在200-300行以内,出现以下情况时考虑拆分:
  • 多个独立的UI模块
  • 复杂的条件渲染逻辑
  • 多个无关联的
    useState
    调用
  • 代码难以一眼理解
将子组件和父组件放在同一目录下,避免桶文件(barrel re-export)的重导出。
将重复的JSX模式提取为小组件。

Data Fetching

数据请求

All data fetching uses TanStack Query (React Query). See
studio-queries
skill for query/mutation patterns and
studio-error-handling
skill for error display conventions.
所有数据请求使用TanStack Query(React Query)。参考
studio-queries
skill 了解查询/变更模式,参考
studio-error-handling
skill 了解错误展示规范。

Loading / Error / Success Pattern

加载/错误/成功模式

Top level:
tsx
const { data, error, isLoading, isError, isSuccess } = useQuery(...)

if (isLoading) return <GenericSkeletonLoader />
if (isError) return <AlertError error={error} subject="Failed to load data" />
if (isSuccess && data.length === 0) return <EmptyState />
return <DataDisplay data={data} />
Use early returns — avoid deeply nested conditionals.
Inline:
tsx
<div>
  {isLoading && <InlineLoader />}
  {isError && <InlineError error={error} />}
  {isSuccess && data.length === 0 && <EmptyState />}
  {isSuccess && data.length > 0 && <DataDisplay data={data} />}
</div>
顶层写法:
tsx
const { data, error, isLoading, isError, isSuccess } = useQuery(...)

if (isLoading) return <GenericSkeletonLoader />
if (isError) return <AlertError error={error} subject="Failed to load data" />
if (isSuccess && data.length === 0) return <EmptyState />
return <DataDisplay data={data} />
使用提前返回,避免深层嵌套的条件语句。
行内写法:
tsx
<div>
  {isLoading && <InlineLoader />}
  {isError && <InlineError error={error} />}
  {isSuccess && data.length === 0 && <EmptyState />}
  {isSuccess && data.length > 0 && <DataDisplay data={data} />}
</div>

State Management

状态管理

Keep state as local as possible; lift only when needed.
Group related form state with
react-hook-form
rather than multiple
useState
calls. See
studio-ui-patterns
skill for form layout and component conventions.
tsx
// ❌ multiple related useState
const [name, setName] = useState('')
const [email, setEmail] = useState('')

// ✅ grouped with react-hook-form
const form = useForm<FormValues>({ defaultValues: { name: '', email: '' } })
尽可能保持状态本地化,仅在必要时提升状态。
使用
react-hook-form
分组管理关联的表单状态,而不是多个
useState
调用。参考
studio-ui-patterns
skill 了解表单布局和组件规范。
tsx
// ❌ 多个关联的useState
const [name, setName] = useState('')
const [email, setEmail] = useState('')

// ✅ 使用react-hook-form分组
const form = useForm<FormValues>({ defaultValues: { name: '', email: '' } })

Custom Hooks

自定义Hook

Extract complex or reusable logic into hooks. Return objects, not arrays:
tsx
// ❌ array return (hard to extend)
return [value, toggle]

// ✅ object return
return { value, toggle, setTrue, setFalse }
将复杂或可复用的逻辑提取为Hook,返回对象而非数组:
tsx
// ❌ 数组返回(难以扩展)
return [value, toggle]

// ✅ 对象返回
return { value, toggle, setTrue, setFalse }

Event Handlers

事件处理器

  • Prop callbacks:
    on
    prefix (
    onClose
    ,
    onSave
    )
  • Internal handlers:
    handle
    prefix (
    handleSubmit
    ,
    handleCancel
    )
Use
useCallback
for handlers passed to memoized children; avoid unnecessary inline arrow functions.
  • 回调Prop:使用
    on
    前缀(
    onClose
    onSave
  • 内部处理器:使用
    handle
    前缀(
    handleSubmit
    handleCancel
向经过memo优化的子组件传递处理器时使用
useCallback
,避免不必要的行内箭头函数。

Conditional Rendering

条件渲染

tsx
// Simple show/hide
<>{isVisible && <Component />}</>

// Binary choice
<>{isLoading ? <Spinner /> : <Content />}</>

// Multiple conditions — use early returns, not nested ternaries
if (isLoading) return <Spinner />
if (isError) return <Error />
return <Content />
tsx
// 简单显示/隐藏
<>{isVisible && <Component />}</>

// 二选一渲染
<>{isLoading ? <Spinner /> : <Content />}</>

// 多条件场景:使用提前返回,不要嵌套三元运算符
if (isLoading) return <Spinner />
if (isError) return <Error />
return <Content />

Performance

性能优化

useMemo
for genuinely expensive computations (measured, not assumed). Don't wrap everything — only optimize when you have a measured problem or are passing values to memoized children.
useMemo
仅用于确实开销大的计算(需经过实际测量,而非主观假设)。不要所有代码都包裹,仅当你检测到实际性能问题,或是向memo化的子组件传递值时才进行优化。

TypeScript

TypeScript

Define prop interfaces explicitly. Use discriminated unions for complex state:
tsx
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error }
Avoid
as any
/
as Type
casts. Validate at boundaries with zod:
tsx
// ❌ type cast
const user = apiResponse as User

// ✅ zod parse
const user = userSchema.parse(apiResponse)
// or safe:
const result = userSchema.safeParse(apiResponse)
明确定义Props接口,复杂状态使用可辨识联合类型:
tsx
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error }
避免使用
as any
/
as Type
类型断言,在边界处使用zod做数据校验:
tsx
// ❌ 类型断言
const user = apiResponse as User

// ✅ zod校验解析
const user = userSchema.parse(apiResponse)
// 或者安全解析:
const result = userSchema.safeParse(apiResponse)

Testing

测试

Extract logic into
.utils.ts
pure functions and test exhaustively. See the
studio-testing
skill for the full testing strategy and decision tree.
将逻辑提取到
.utils.ts
的纯函数中并进行全面测试。参考
studio-testing
skill 了解完整的测试策略和决策树。