1. 组件 & 文件
说明: 组件是 UI 单元,RN 社区约定用 PascalCase(每个单词首字母大写)。文件名最好和组件名一致,这样在编辑器里搜 Detail 能同时找到文件和组件。
// page/Detail/index.js
import React from 'react'
import { View, Text } from 'react-native'
function Detail({ navigation }) {
return (
<View>
<Text>详情页</Text>
</View>
)
}
export default Detail
-
Detail→ 组件名,PascalCase -
index.js→ 目录名Detail与组件名对应,这是 RN 项目常见写法
2. Hook
说明: 自定义 Hook 必须以 use 开头,这是 React 的规定。React 靠这个名字判断你是否在 Hook 里调用了 useState、useEffect 等。命名用 camelCase,后面跟「这个 Hook 做什么」。
// request/detail.js
import { useQuery } from '@tanstack/react-query'
import fetchApi from '../useHooks/useFetch'
function useDetail(cookId) {
return useQuery({
queryKey: ['cook', 'detail', cookId],
queryFn: () => fetchApi.get(`/cookDetail/${cookId}`),
})
}
export default { useDetail }
-
useDetail→use表示 Hook,Detail表示「获取详情数据」 - 调用时:
const detailQuery = useDetail(params.id)
3. export / import(最容易混)
说明: 导出有两种方式,决定了导入时名字能不能自己取。
3.1 默认导出 export default
导出的是一个「默认模块」,导入时名字随便起。
// request/detail.js — 导出方
function useDetail(cookId) {
return useQuery({
queryKey: ['cook', 'detail', cookId],
queryFn: () => fetchApi.get(`/cookDetail/${cookId}`),
})
}
function useCollection() {
const queryClient = useQueryClient()
const collection = useMutation({
mutationFn: (cookId) => fetchApi.get(`/userCollection/${cookId}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['cook', 'detail'] })
},
})
return { collection }
}
export default {
useDetail,
useCollection,
}
// page/Detail/index.js — 使用方
import useData from '../../request/detail.js'
function DetailPage({ navigation }) {
const { params } = navigation.state
// useData 只是 import 时起的别名
// detail.js 里并没有名叫 useData 的变量
const detailQuery = useData.useDetail(params.id)
const { collection } = useData.useCollection()
return null
}
下面三种写法导入的是同一个对象:
import useData from '../../request/detail.js'
import detailApi from '../../request/detail.js'
import request from '../../request/detail.js'
useData.useDetail(1) // 一样
detailApi.useDetail(1) // 一样
request.useDetail(1) // 一样
3.2 命名导出 export { xxx }
导出时用 {} 包住名字,导入时必须用相同名字(或用 as 改名)。
// utils/format.js — 导出方
export function formatTime(minutes) {
return `${minutes}分钟`
}
export const MAX_COOK_TIME = 120
// 使用方
import { formatTime, MAX_COOK_TIME } from '../utils/format.js'
formatTime(30) // "30分钟"
MAX_COOK_TIME // 120
// 如果想改名,用 as
import { formatTime as fmtTime } from '../utils/format.js'
fmtTime(30) // "30分钟"
3.3 两种方式对比
| 导出方式 | 导入写法 | 名字能否自取 |
|---|---|---|
export default obj |
import 任意名 from '...' |
能 |
export function foo() |
import { foo } from '...' |
不能,必须叫 foo
|
export function foo() |
import { foo as bar } from '...' |
用 as 改成 bar
|
4. Props(父子组件传参)
说明: Props 是父组件传给子组件的参数,用 camelCase。约定俗成的后缀:
- 普通数据:直接名词,如
name、id - 布尔值:
is/has/show开头,如isVisible、userCollected - 回调函数:
on开头,如onPress、onCollection(表示「当 xxx 发生时调用我」)
子组件 — 声明接收哪些 props:
// page/Detail/components/Bottom.js
import React from 'react'
import { View, Text, TouchableOpacity } from 'react-native'
function Bottom({
id, // 普通数据
name, // 普通数据
userCollected, // 布尔:是否已收藏
onComment, // 回调:点击评论时触发
onCollection, // 回调:点击收藏时触发
}) {
return (
<View>
<Text>{name}</Text>
<TouchableOpacity onPress={onComment}>
<Text>评论</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => onCollection(() => {})}>
<Text>{userCollected ? '已收藏' : '收藏'}</Text>
</TouchableOpacity>
</View>
)
}
export default Bottom
父组件 — 传入具体值:
// page/Detail/index.js
import Bottom from './components/Bottom'
import useData from '../../request/detail.js'
function DetailPage({ navigation }) {
const { params } = navigation.state
const detailQuery = useData.useDetail(params.id)
const { collection, cancelCollection } = useData.useCollection()
const bottomProps = {
id: params.id,
name: detailQuery.data?.name,
userCollected: detailQuery.data?.isCollection,
onComment: () => {
setModalVisible(true)
},
onCollection: (callback) => {
if (detailQuery.data?.isCollection) {
cancelCollection.mutateAsync(params.id).then(callback)
} else {
collection.mutateAsync(params.id).then(callback)
}
},
}
return <Bottom {...bottomProps} />
}
5. 事件函数
说明: 同一个函数,在组件内部定义和作为 prop 传给子组件时,常用不同前缀,方便区分「谁定义的」和「谁触发的」:
| 位置 | 前缀 | 含义 |
|---|---|---|
| 组件内部 | handle |
我自己写的处理逻辑 |
| 传给子组件 | on |
告诉子组件「发生 xxx 时调这个」 |
function DetailPage() {
const [modalVisible, setModalVisible] = useState(false)
// 内部定义:handle 开头
const handleComment = () => {
setModalVisible(true)
}
const handleCollection = (callback) => {
collection.mutateAsync(params.id).then(callback)
}
return (
<Bottom
onComment={handleComment} // prop 名用 on
onCollection={handleCollection} // 函数体用 handle
/>
)
}
6. State & Ref
说明:
-
State 用
useState,返回[当前值, 修改函数],修改函数固定为set+ 首字母大写的值名 -
Ref 用
useRef,命名常以Ref结尾,表示「某个 DOM/组件的引用」
function DetailPage({ navigation }) {
const { params } = navigation.state
const detailQuery = useData.useDetail(params.id)
// state:存会变化、触发重渲染的数据
const [detail, setDetail] = useState(null)
const [modalVisible, setModalVisible] = useState(false)
// ref:存不触发重渲染的引用(DOM、定时器、临时对象等)
const scrollViewRef = useRef(null)
const commentData = useRef({ stars: 1 })
// 接口数据回来后写入 state
useEffect(() => {
if (detailQuery.data) {
setDetail(detailQuery.data)
}
}, [detailQuery.data])
return (
<ScrollView ref={scrollViewRef}>
<Text>{detail?.name}</Text>
</ScrollView>
)
}
7. Style 样式
说明: RN 的样式对象是 JS 对象,属性名必须用 camelCase(不能写 CSS 的 font-size,要写 fontSize)。样式名按用途起,不要按颜色值起。
import { StyleSheet, View, Text } from 'react-native'
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 16,
},
title: {
fontSize: 18,
color: '#121212',
},
subtitle: {
fontSize: 14,
color: '#777777',
},
activeTab: {
fontWeight: '500',
color: '#AB8C5E',
},
})
function DetailPage() {
const isSelected = true
return (
<View style={styles.container}>
<Text style={styles.title}>红烧肉</Text>
<Text style={[styles.subtitle, isSelected && styles.activeTab]}>
简单
</Text>
</View>
)
}
-
styles.title→ 标题样式 -
[styles.subtitle, isSelected && styles.activeTab]→ 数组写法,可叠加多个样式
8. 常量
说明: 整个应用不变、多处复用的值,用 UPPER_SNAKE_CASE(全大写 + 下划线),一眼看出「这是常量,不要改」。
// useHooks/deviceProtocol/protocol.js
export const EVENT_LIST_NAME = {
collectionCookControl: 'collectionCookControl',
}
// useHooks/useFetch/index.js
const API_BASE_URL = 'https://dreamecook.xin/cookbook/appPlugin'
// 使用
import { EVENT_LIST_NAME } from '../../useHooks/deviceProtocol/protocol.js'
deviceState.sendAction({
name: EVENT_LIST_NAME.collectionCookControl,
value: `1,${params.cIndex}`,
})
React Query 的 queryKey 虽不是常量写法,但也是固定字符串数组,用于标识缓存:
queryKey: ['cook', 'detail', cookId]
queryKey: ['category', 'cook', params]
9. 目录结构
说明: 目录名反映内容类型。页面和组件用 PascalCase(和组件名一致),工具、Hook、请求层用 camelCase。
main/
├── page/
│ └── Detail/ # 页面,PascalCase
│ ├── index.js # 导出 Detail 组件
│ └── components/
│ └── Bottom.js # 页面私有子组件
├── components/
│ └── MyModal/ # 公共组件,PascalCase
│ └── index.js
├── request/
│ └── detail.js # 接口 Hook,camelCase
├── useHooks/
│ └── useFetch/
│ └── index.js # 请求封装
└── utils/
└── UIConfig.js # 工具函数
10. 速查表
| 场景 | 风格 | 示例 | 说明 |
|---|---|---|---|
| 组件 | PascalCase | function Detail() {} |
UI 单元 |
| Hook |
use + camelCase |
function useDetail(id) {} |
必须以 use 开头 |
| default import | 自取 | import useData from './detail.js' |
名字与导出方无关 |
| 命名 import | 固定 | import { useQuery } from '...' |
名字必须对应 |
| props 数据 | camelCase | name={data.name} |
普通传参 |
| props 布尔 | is/has | userCollected={true} |
表示 true/false |
| props 回调 | on | onCollection={fn} |
事件回调 |
| 内部函数 | handle | const handlePress = () => {} |
组件内定义 |
| state | camelCase | [detail, setDetail] |
会触发重渲染 |
| ref | xxxRef | scrollViewRef |
不触发重渲染 |
| style | camelCase | title: { fontSize: 18 } |
RN 样式对象 |
| 常量 | UPPER_SNAKE | API_BASE_URL |
全局不变值 |