本文来自Medium上Aleksey Kozin的文章,原文地址:Double Your React Coding Speed With This Simple Trick,本文主要内容为如何编写可复用的react组件,文章有删改,推荐react新手观看,有能力的同学建议观看原文。
如果你使用过react或者react native,应该感受过写代码的速度在减慢,导致这样的原因有很多,比如你会花费更多的时间在处理bug上,单个文件写了大量的业务代码并且维护起来非常麻烦,还有大量重复的逻辑,但你可以通过重构可复用的代码块来提升编码速度。
你将会在这篇文章学到什么?
我们从一个普通的React组件出发,一步步的重构成更高级的组件。希望这篇文章能为你带来收获,提高你的编码速度。
我们将重构的React原始组件
一个简单像我们平时开发的组件,大概会包括以下内容:
- 从后端获取列表数据
- 当页面需要加载时展示"loading"
- 将已加载的数据用列表(列表卡片)的形式展示
- 当用户点击列表的卡片时会展示弹窗并展示详情内容
这像不像你写的代码?
import React, { useEffect, useState } from 'react';
import { FlatList, Text, View, StyleSheet, Modal, TouchableOpacity } from 'react-native';
import AddModal from '../components/AddModal';
import LoadingIndicator from '../components/LoadingIndicator'
import BrowserItem from '../components/BrowserItem'
import colors from '../config/colors';
function Browsers() {
const URL = 'https://google.com/myData.json'
// Element of json
// {"Browsers":[
// {
// "fullname": "Chrome",
// "linkToBrowser": "https://google.com",
// "image": "https://linktoimage.com/chrome.png",
// "minMemory": "1 GB",
// "currentVersion": "29.0.1",
// "minimumRAM": "2 GB",
// "description": "How much RAM do you have? Ha-ha",
// "windows": true,
// "mac": true,
// "linux": true,
// "ubuntu": true,
// "fedora": false,
// "stars": 4,
// "id":"chrome"
// },
// ...
// ]
// }
const [loading, setLoading] = useState(true)
const [browsers, setBrowsers] = useState([])
const [modalVisible, setModalVisible] = useState(false)
const [description, setDescription] = useState("")
const changeDescription = (description) => {
setDescription(description)
setModalVisible(!modalVisible)
}
const changeOpacity = () => {
setModalVisible(!modalVisible)
console.log('changeOpacity')
}
useEffect(() => {
fetch(URL)
.then((response) => response.json())
.then((responseJson) => {
return responseJson.Browsers
})
.then(browsers => {
setBrowsers(browsers)
// console.log(browsers)
setLoading(false)
})
.catch(error => {
console.log(error)
})
.finally(() => setLoading(false));
}, [])
return (
<View style={styles.container}>
{loading ? (
<LoadingIndicator />
) : (
<View>
<AddModal
modalVisible={modalVisible}
changeOpacity = {() => changeOpacity()}
description={description}
/>
<FlatList
data={browsers}
keyExtractor={browser => browser.fullname}
renderItem={({ item }) =>
<BrowserItem
fullname={item.fullname}
image={item.image}
linkToBrowser={item.linkToBrowser}
minMemory={item.minMemory}
currentVersion={item.currentVersion}
minimumRAM={item.minimumRAM}
description={item.description}
windows={item.windows}
mac={item.mac}
linux={item.linux}
ubuntu={item.ubuntu}
fedora={item.fedora}
stars={item.stars}
changeDescription={() => changeDescription(item.description)}
/>
}
/>
</View>
)
}
</View >
);
};
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center'
},
})
export default Browsers;
先来说说这段代码的优点
- 它能工作,具有完整的下载和展示给用户的功能。
- 它使用了React Hooks
这段代码的缺点
这个组件被设计成只能在一个App的一处地方使用一次(不能复用)
记住这个口头禅:
成为快速的开发者需要掌握开发可复用代码块的艺术
可复用的代码块和方法可以为你带来:
- 减少书写代码的时间
- 减少查找bug的时间
- 减少阅读代码的时间
- 对你复用的代码做更少的测试
如何使上面的那段代码变得可复用?
将常量移动到组件的props中
function Browsers({url = 'https://google.com/myData.json'}) {
const URL = url
...
}
我们将常量URL移动到props中,这样我们就通过传入不同的URL复用组件。我们只做了一点小改动,产生了不错的效果。
热身结束,接下来我们来做点儿更cool的东西。
分离业务逻辑和视觉逻辑
这是这篇文章最重要的部分。如果你能从这篇文章学到一个东西,那一定是:
成为更快的React开发者一定是掌握了分离业务逻辑和视觉逻辑的人。
业务逻辑:处理和保存状态,比如<Browser />组件中写在return上面的代码。
UI逻辑:所有展示在显示器上的内容以及读取用户的输入,比如return中的所有代码。
让我们从分离这两部分逻辑开始,来看如何将我们的代码变得可复用。
我们将做什么:
创建一个自定义React HookuseBrowsers()来存放业务逻辑
- 创建React组件
<BrowsersList />,使用props来处理视觉逻辑。
import React, {useEffect, useState} from 'react'
import {FlatList, StyleSheet, View} from 'react-native'
import AddModal from '../components/AddModal'
import LoadingIndicator from '../components/LoadingIndicator'
import BrowserItem from '../components/BrowserItem'
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
},
})
function useBrowsers(url) {
const [loading, setLoading] = useState(true)
const [browsers, setBrowsers] = useState([])
const [modalVisible, setModalVisible] = useState(false)
const [description, setDescription] = useState('')
const changeDescription = (description) => {
setDescription(description)
setModalVisible(!modalVisible)
}
const changeOpacity = () => {
setModalVisible(!modalVisible)
console.log('changeOpacity')
}
useEffect(() => {
fetch(URL)
.then((response) => response.json())
.then((responseJson) => {
return responseJson.Browsers
})
.then((browsers) => {
setBrowsers(browsers)
// console.log(browsers)
setLoading(false)
})
.catch((error) => {
console.log(error)
})
.finally(() => setLoading(false))
}, [])
return {
loading,
browsers,
modalVisible,
description,
changeDescription,
changeOpacity,
}
}
function BrowsersList({
loading,
browsers,
modalVisible,
description,
changeDescription,
changeOpacity,
}) {
return (
<View style={styles.container}>
{loading ? (
<LoadingIndicator />
) : (
<View>
<AddModal
modalVisible={modalVisible}
changeOpacity={() => changeOpacity()}
description={description}
/>
<FlatList
data={browsers}
keyExtractor={(browser) => browser.fullname}
renderItem={({item}) => (
<BrowserItem
fullname={item.fullname}
image={item.image}
linkToBrowser={item.linkToBrowser}
minMemory={item.minMemory}
currentVersion={item.currentVersion}
minimumRAM={item.minimumRAM}
description={item.description}
windows={item.windows}
mac={item.mac}
linux={item.linux}
ubuntu={item.ubuntu}
fedora={item.fedora}
stars={item.stars}
changeDescription={() => changeDescription(item.description)}
/>
)}
/>
</View>
)}
</View>
)
}
function Browsers() {
return <BrowsersList {...useBrowsers('https://google.com/myData.json')} />
}
export default Browsers
我们对代码做了一些移动,虽然离完美还很遥远,但它确实变得更容易复用了:
- 我们可以让
<BrowserList>使用不同的数据源。之前我们只能接受传入的HTTP请求的数据,现在我们可以接受来自内存,硬盘或者其它地方的数据了。 - 我们可以在其它地方使用useBrowsers()了。
将代码分割成更小的可复用文件
首先,将代码分成两部分更有利于我们重构。将代码分成不同的文件/模块更有利于代码的阅读。这也可以使开发者更容易理解每个模块的代码是独立的。每个模块也更容易理解和复用。
这是个典型的React项目结构:
index.js从Browsers.jsx导出<Browsers />组件。components和hooks文件夹的代码与<Browsers/>组件相关。BrowsersList.jsx也可以继续创建与之相关的hooks,components以及index
React 项目结构像是一个递归树
关于更多React项目结构的内容可以看David Gilbertson
接下来看BrowsersList.jsx:
import React from 'react'
import {FlatList, StyleSheet, View} from 'react-native'
import AddModal from '../../../components/AddModal'
import LoadingIndicator from '../../../components/LoadingIndicator'
import BrowserItem from '../../../components/BrowserItem'
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
},
})
export function BrowsersList({
loading,
browsers,
modalVisible,
description,
changeDescription,
changeOpacity,
}) {
return (
<View style={styles.container}>
{loading ? (
<LoadingIndicator />
) : (
<View>
<AddModal
modalVisible={modalVisible}
changeOpacity={() => changeOpacity()}
description={description}
/>
<FlatList
data={browsers}
keyExtractor={(browser) => browser.fullname}
renderItem={({item}) => (
<BrowserItem
fullname={item.fullname}
image={item.image}
linkToBrowser={item.linkToBrowser}
minMemory={item.minMemory}
currentVersion={item.currentVersion}
minimumRAM={item.minimumRAM}
description={item.description}
windows={item.windows}
mac={item.mac}
linux={item.linux}
ubuntu={item.ubuntu}
fedora={item.fedora}
stars={item.stars}
changeDescription={() => changeDescription(item.description)}
/>
)}
/>
</View>
)}
</View>
)
}
ok,代码变得更少了,也没有这么杂乱无章了。现在我们可以集中精力重构这部分的代码。
但在处理这部分代码之前先要解决一个设计上的大问题。
关于复用组件的一个大问题
先看一下<BrowsersList />组件函数的签名:
- 如果我们重命名一些props,比如说
changeDescription改为setSelectedBrowser或者description改为browser。 - 或者我们删除一些props
- 或者我们添加一些props
每个改动都会导致<BrowsersList / >组件崩溃。
每次修改组件的签名,每个引用这个组件的地方就会报错。你会经常修改签名,毕竟你不会一开始就把组件设计好。
你的IDE不能在你重构的过程中自动重命名。你不得不手动修改每个引用组件的地方。这将导致你编码的速度变慢。
最惨的部分是你不知道哪里会崩溃。唯一能捕获到错误的方法是:
- 运行app
- 手动跳转到崩溃的组件
- 报错
- 阅读报错信息
- 修改bug然后再试一次
这是个又慢又乏味的任务,但这种情况又经常在生产环境上发生。
来看我们的Browsers.jsx文件,它会报错吗?
import React from 'react'
import {BrowsersList} from './components/BrowsersList'
import {useBrowsers} from './hooks/useBrowsers'
export function Browsers() {
return <BrowsersList {...useBrowsers('https://google.com/myData.json')} />
}
谁知道呢!需要对比BrowsersList.jsx和useBrowsers.js的参数才能确定。
这会崩溃吗?
// useBrowsers.js
...
return {
loading,
browsers,
modalVisible,
descripton,
changeDescription,
changeOpacity,
}
...
// BrowsersList.jsx
...
export function BrowsersList({
loading,
browsers,
modalVisible,
description,
changeDescription,
changeOpacity,
}) {
....
当然会,因为useBrowsers.js中的descripton缺少了一个'i',是个错字。
许多初级开发者花费很长时间去debug试图去寻找哪里出了问题,结果发现是props值时出了问题(本该使用string却使用了number)。这里有更快的解决这个问题的方法。
停止使用JS开发,开始拥抱TypeScript吧
2021年每个React/React Native的开发者都没有理由不使用TypeScript。
TypeScript很cool的特点
更少的bugs
更少的bugs = 更快的编码速度
通过TS我们很轻松的找到了错字:
你的IDE会自动提示你React Pros的参数,当你使用第三方库并且不知道props的名称时这将节省你大量时间:
你将会获得"重构名称"的新技能点。这将节省你重命名props的时间,并且一次修改后其它使用这个props的地方也会自动重命名。
你也不会再忘记添加null/undefined的检查了。
当然,在我们输入错误的props时我们也会接受到高亮的错误:
TS能轻松的为你减少时间的同时也会减少debug的压力。
设计流畅开发系统第一步:声明types
tips:本文不是TS的教程。如果你有不理解的地方,先简单了解一下TypeScript再回来看本文
让我们重新回到<BrowsersList />组件并且声明props的types(不要忘了将你的.jsx重命名为.tsx)。
// ...
export type Browser = {
fullname: string // we expect "fullname" Browser field to be a string
image: string
linkToBrowser: string
minMemory: string
currentVersion: string
minimumRAM: string
description: string
windows: boolean
mac: boolean
linux: boolean
ubuntu: boolean
fedora: boolean
stars: number
}
export type BrowsersListProps = {
loading: boolean
browsers: Browser[] // we expect "browsers" prop to be an array of Browser
modalVisible: boolean
description: string
// we expect "changeDescription" prop to be a function
// taking a string as an input and returning nothing
changeDescription: (description: string) => void
// we expect "changeOpacity" prop to be a function
// taking nothing and returning nothing
changeOpacity: () => void
}
export function BrowsersList({
loading,
browsers,
modalVisible,
description,
changeDescription,
changeOpacity,
}: BrowsersListProps) {
return (
// ...
之后,更新useBrowsers()的签名:
import {useEffect, useState} from 'react'
import {BrowsersListProps} from '../components/BrowsersList'
export function useBrowsers(url: string): BrowsersListProps {
const [loading, setLoading] = useState(true)
// ...
现在TS会校验useBrowsers()和BrowsersList的参数是否兼容。如果我们修改过BrowsersList的props,我们会得到报错。仅此一项就可以保证减少生产环境中的错误。
快速的系统架构
BrowsersListProps目前看起来还是很乱:
export type BrowsersListProps = {
loading: boolean
browsers: Browser[]
modalVisible: boolean
description: string
changeDescription: (description: string) => void
changeOpacity: () => void
}
- 组件要展示loading的状态,需要占用一行type声明
- 组件要展示Browsers的数组,需要占用一行type声明
- 组件要展示模态框的详细信息,这将占用四行type声明。模态框的详情可能会需要更多的字段(也许在将来),这种定义方法将限制我们展示更多的字段,记住,修改组件的签名的代价是非常高的。
我们可以缩短type的声明并重构,并且用Browser的type重构模态框的特性。
这将减少props的复杂性:
这个小的重构练习向你证明了TS的一个显著特征:快速的系统设计。
types非常简单。它不需要写很多代码,但可以为系统提供很多有用的信息,因此你不需要写太多的代码就可以使用types并且设计你的代码。减少你进行架构设计的时间,也可以推动你设计出更易用,更快的系统。
声明types之后开始填充组件主体
让我们修复我们的<BrowsersList />组件以便它可以处理BrowsersListProps的签名。我们可以重构<BrowserItem>以便它可以只使用两个参数。这将使代码变得更加易读和快速。
import React from 'react'
import {FlatList, StyleSheet, View} from 'react-native'
import AddModal from '../../../components/AddModal'
import LoadingIndicator from '../../../components/LoadingIndicator'
import BrowserItem from '../../../components/BrowserItem'
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
},
})
export type Browser = {
fullname: string
image: string
linkToBrowser: string
minMemory: string
currentVersion: string
minimumRAM: string
description: string
windows: boolean
mac: boolean
linux: boolean
ubuntu: boolean
fedora: boolean
stars: number
}
export type BrowsersListProps = {
loading: boolean
browsers?: Browser[]
selectedBrowser?: Browser
setSelectedBrowser: (browser?: Browser) => void
}
export function BrowsersList(props: BrowsersListProps) {
const {loading, selectedBrowser, setSelectedBrowser, browsers} = props
return (
<View style={styles.container}>
{loading ? (
<LoadingIndicator />
) : (
<View>
<AddModal
modalVisible={Boolean(selectedBrowser)}
onClose={() => setSelectedBrowser(undefined)}
description={selectedBrowser?.description}
/>
<FlatList
data={browsers}
keyExtractor={(browser) => browser.fullname}
renderItem={({item}) => (
<BrowserItem
browser={item}
onPress={() => setSelectedBrowser(item)}
/>
)}
/>
</View>
)}
</View>
)
}
这个组件已经看起来更加可读而且不那么可怕了。
从中抽离出可复用的逻辑
开发者通过观察组件会有不同的感受。比如,我觉得在中展示loading的状态是一项非常cool的功能,因为它可以在不同的组件中复用。
我们创建<UIFriendlyList/>组件取代<FlatList/>。这个新组件具有展示loading状态的能力。
与之前一样我们要创建类型:
type UIFriendlyListProps<T> = FlatListProps<T> & {loading?: boolean}
- "T"是一个参数类型。参数的类型被叫做泛型。"T"和"UIFriendlyList"的关系和"arg"和"foo(arg)"方法的关系相同。如果你想依据一个类型构造另一个类型,可以使用泛型。
- "&"是一个交叉类型。
type X = A & B意味着X包含A和B。
关于这次改动:
- 我们声明了UIFriendlyLsitProps的声明。
- 定义了一个泛型
- UIFriendlyListProps 扩展了 FlatListProps的 'loading' 状态
- 我们通过
FlatListProps和{loading?: boolean}交叉类型定义了UIFriendlyListProps
让我们以此为主体创建一个UIFriendlyList.jsx文件:
import React from 'react'
import {FlatList, FlatListProps, Text} from 'react-native'
import LoadingIndicator from './LoadingIndicator'
export type UIFriendlyListProps<T> = FlatListProps<T> & {loading?: boolean}
export function UIFriendlyList<T>(props: UIFriendlyListProps<T>) {
if (props.loading) {
return <LoadingIndicator />
}
if (props?.data && props.data.length === 0) {
return <Text>This list is empty (</Text>
}
return <FlatList {...props} />
}
我们还添加了一个空的状态。所以当用户知道列表为空时就不需要等待接口的请求了,非常友好的设计。
现在,让我们确认下:
import React from 'react'
import {StyleSheet, View} from 'react-native'
import AddModal from '../../../components/AddModal'
import BrowserItem from '../../../components/BrowserItem'
import {UIFriendlyList} from '../../../components/UIFriendlyList'
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
},
})
export type Browser = {
fullname: string
image: string
linkToBrowser: string
minMemory: string
currentVersion: string
minimumRAM: string
description: string
windows: boolean
mac: boolean
linux: boolean
ubuntu: boolean
fedora: boolean
stars: number
}
export type BrowsersListProps = {
loading: boolean
browsers?: Browser[]
selectedBrowser?: Browser
setSelectedBrowser: (browser?: Browser) => void
}
export function BrowsersList(props: BrowsersListProps) {
const {loading, selectedBrowser, setSelectedBrowser, browsers} = props
return (
<View style={styles.container}>
<AddModal
modalVisible={Boolean(selectedBrowser)}
onClose={() => setSelectedBrowser(undefined)}
description={selectedBrowser?.description}
/>
<UIFriendlyList
loading={loading}
data={browsers}
renderItem={({item}) => (
<BrowserItem
key={item.fullname}
browser={item}
onPress={() => setSelectedBrowser(item)}
/>
)}
/>
</View>
)
}
与之前的初始代码相比,现在的组件更容易理解了。 “抽象可复用的代码”是一项无休止的工作,所以一旦你觉得效果达到了预期,就该停止,去做其它工作了。
接下来,我们该把注意力放在业务逻辑上,开始处理useBrowsers() hook。
重构业务逻辑
我们让useBrowsers()返回一个BrowsersListProps类型的对象。我也重构了Loading,现在它在fetch前为true,fetch之后变为false。
import {useEffect, useState} from 'react'
import {Browser, BrowsersListProps} from '../components/BrowsersList'
export function useBrowsers(url: string): BrowsersListProps {
const [loading, setLoading] = useState(false)
const [browsers, setBrowsers] = useState<Browser[]>([])
const [selectedBrowser, setSelectedBrowser] = useState<Browser | undefined>(
undefined,
)
useEffect(() => {
setLoading(true)
fetch(url)
.then((response) => response.json())
.then((responseJson) => {
return responseJson.Browsers
})
.then((browsers) => {
setBrowsers(browsers)
})
.catch((error) => {
console.log(error)
})
.finally(() => setLoading(false))
}, [url])
return {
loading,
browsers,
selectedBrowser,
setSelectedBrowser,
}
}
这还没结束,我们还可以更深入的抽象和构建可复用的代码块。“在接口请求数据并赋值的过程中一直在展示loading”这部分代码非常适合处理成代码块,我们可以把它和userBrowsers()分离。
与之前一样我们需要声明一些types。我们需要自定义一个useFetch()hook用来获取数据并且控制loading状态。我们也需要定义从服务端获取数据的结构(FetchBrowsersResults):
export type FetchBrowsersResults = {
Browsers: Browser[]
}
export type UseFetch<T> = {
loading: boolean
// We use Generic. T - is a type argument that can be any type.
// We can useFetch() with any type
// ? means, that T can be undefined
data?: T
}
export function useFetch<T>(url: string): UseFetch<T> {}
我们专门创建了一个useFetch.ts文件用来存放useFetch()的代码:
import {useEffect, useState} from 'react'
import {Alert} from 'react-native'
export type UseFetch<T> = {
loading: boolean
data?: T
}
export function useFetch<T>(url: string): UseFetch<T> {
const [loading, setLoading] = useState<boolean>(false)
const [data, setData] = useState<T | undefined>(undefined)
useEffect(() => {
setLoading(true)
fetch(url)
.then((response) => response.json())
.then(setData)
.finally(() => setLoading(false))
.catch((error) => Alert.alert('Fetch error', error))
}, [url])
return {
loading,
data,
}
}
现在让我们重构useBrowsers()hook:
import {useState} from 'react'
import {Browser, BrowsersListProps} from '../components/BrowsersList'
import {useFetch} from '../../../hooks/useFetch'
export type FetchBrowsersResults = {
Browsers: Browser[]
}
export function useBrowsers(url: string): BrowsersListProps {
const {loading, data} = useFetch<FetchBrowsersResults>(url)
const [selectedBrowser, setSelectedBrowser] = useState<Browser | undefined>(
undefined,
)
return {
loading,
browsers: data?.Browsers,
selectedBrowser,
setSelectedBrowser,
}
}
与之前相比,现在的代码更容易理解了。目前看来,代码也没有进一步抽象的必要了。