Пиши на React в два раза быстрее! Простые трюки для крутого разработчика
Что делать, если скорость разработки уже не та, что раньше? Отказавшись от монолита, изменить подход к написанию кода и начать использовать его повторно! Обсудить Ваша производительность ухудшается, а скорость разработки стремительно падает? Проводите время за отловом багов, вместо того, чтобы писать новые фичи? Не можете найти нужный код в огромных файлах и делаете одно и то же снова и снова? Вам помогут переиспользуемые блоки кода! Переиспользуемые блоки кода – то, что нужно прогрессивному разработчику! Современный фронтенд крепко стоит на компонентах, потому что это действительно разумный и приятный подход к разработке. Собирать программу из «кирпичиков Lego» существенно проще и надежнее, чем с нуля писать монолит. Так бросим же все силы на то, чтобы научиться мыслить в компонентном стиле! Казалось бы, каждый джун сегодня умеет писать компоненты, чему тут учиться? Но компонент компоненту рознь, для наших программ нужны только самые качественные и красивые из них. Поэтому сейчас мы возьмем неограненный React-компонент, который написал простой разработчик вроде вас, и рассмотрим его очень пристально со всех сторон. А затем много-много раз отрефакторим, пока не получится идеальный бриллиант. Будьте готовы к многочисленным озарениям и головокружительному увеличению вашей производительности! Рассмотрим неограненный алмаз, с которым мы будем работать. Этот простой компонент умеет не так уж и много: Этот код похож на ваш? Browsers.js Ну, он работает 🙂 И использует хуки, что само по себе уже неплохо. Это монолит. Не получится взять его и переиспользовать в другом месте или другом приложении. Искусство быстрой разработки – это искусство создания переиспользуемых блоков кода. Чем больше у вас разных кирпичиков, тем быстрее вы можете построить дом. Начнем с самых простых приемов рефакторинга, а затем войдем во вкус и сдеаем что-то посложнее. Browsers.js Одно элементарное действие – и наш компонент уже может работать с другим URL. Маленький шаг для разработчика, но огромный прыжок для переиспользуемого блока! Вероятно, это самая важная фундаментальная установка хорошего разработчика – разделение бизнес-логики от логики отображения. Жизнь становится проще, когда UI компоненты ничего не знают о серверах, логике и состоянии приложения. В чем вообще разница? Бизнес-логика: Код, который принимает решения и изменяет состояния. Все внутри Логика отображения: код, который отображает состояние приложения на экране и взаимодействует с пользователем. Все внутри инструкции Давайте произведем радикальное рассечение компонента на две части и убедимся, что жизнь действительно стала проще: Browsers_splitted.js Код все еще далек от идеала, но уже стал немного более удобным. Разделение монолита на две части открыло широкий простор для нового рефакторинга. Чтобы программа стала еще более читаемой и годной к переиспользованию, разделим код на отдельные изолированные модули. Как говорится, чем изолированнее, тем реюзабельнее. Это файловая структура типичного React-проекта: BrowsersList.jsx можно развить до самостоятельной папки с собственными хуками, компонентами и файлом index.js Посмотрим на BrowsersList.jsx: Этот код уже почти помещается в один экран вашего редактора. Меньше хлама – легче читать! Прежде чем волшебным образом превратить этого гадкого утенка в реюзабельного прекрасного лебедя, давайте поговорим о Большой Проблеме. Взгляните на сигнатуру функции Все сломается! Каждый раз когда вы меняете сигнатуру компонента, возникает проблема во всех местах, где он используется. А вы не раз будете менять эту несчастную сигнатуру, потому что с первого раза редко получается хорошо. Ваша IDE это не отследит, придется ручками искать все вызовы и вносить правки. Это медленно и совсем не круто. Даже более: искать теперь придется по разным файлам, связь между которыми не всегда очевидна. Ну и где профит-то, спросите вы? Опять что-нибудь где-нибудь забудем исправить – и прощай продакшн. Посмотрите внимательно, все ли тут в порядке? Файл useBrowsers.js: Файл BrowsersList.jsx: Бинго! Одна маленькая опечатка – и все идет кувырком. Сколько прекрасных часов проводят молодые разработчики, сравнивая пропсы и пытаясь понять, что же сломалось – но мы-то с вами уже не такие! Давайте решать проблему радикально. В 2021 году у вас нет оправданий, если вы не используете TypeScript. Вам даже не требуется сразу же изучить все его возможности, ведь можно просто добавить TS в проект, не меняя ни строчки, и постепенно наращивать его применение. 38% багов на Airbnb можно было предотвратить с помощью TypeScript С TypeScript ваша IDE обогатится несколькими крутыми фичами: TypeScript сэкономит вам тонну времени и километры нервов. А вот, кстати, и наша опечатка: Так как это не полноценный гайд по TypeScript, разбираться в основах мы сейчас не будем. Все желающие могут заглянуть на typescriptlang.org (или прочитать статью в «Библиотеке программиста» – прим. ред.). Вернемся к BrowsersList_props_types.tsx Теперь обновим сигнатуру useBrowsers.ts И теперь TypeScript будет строго следить, чтобы props.tsx Мы можем уменьшить сложность этого фрагмента и отрефакторить функциональность модального окна для использования типа TypeScript указывает на некорректность сигнатуры при ее изменении Этот маленький рефакторинг должен продемонстрировать вам замечательную способность TypeScript: быстро проектировать системный дизайн. Писать типы очень просто. Они маленькие, но содержат много полезной информации о вашей системе. Фактически вы можете описать вашу программу без кода, одними типами. Теперь поправим Отрефакторим BrowsersList.tsx Компонент стал проще и надежнее. Если вы немного помедитируете на компонент, то на вас снизойдет просветление. Список с “состоянием загрузки” – это очень крутая и востребованная функциональность, которая наверняка будет использоваться везде, где только можно. Сейчас эта функциональность интегрирорована в Как всегда начнем с определения типов: Итак, что мы сделали: Теперь вынесем новый компонент в отдельный файл UIFriendlyList.jsx: UIFriendlyList.tsx Обратите внимание, мы добавили еще пустое состояние, чтобы показать юзеру, что список действительно пуст и он может больше не ждать загрузки. Это прекрасное решение с точки зрения UX. Компонент Во что теперь превратился наш BrowsersList.tsx Сравните его с исходным вариантом. Он почти в два раза меньше и гораздо проще для понимания. К тому же в качестве бонуса вы получили отличный компонент Процесс извлечения переиспользуемых фрагментов прекрасен, но бесконечен. В погоне за сомнительным идеалом вы можете потратить очень много времени в ущерб реальной продуктивности, поэтому нужно уметь останавливать себя. Мы закончили с логикой отображения, пора вернуться к бизнес-логике. Хук useBrowsers.ts Приглядимся повнимательнее. Функциональность “обратиться по некоторому URL и сохранить результат запроса, пока отображается индикатор загрузки” кажется весьма полезной, давайте извлечем ее в новый хук Как всегда, сначала типы. Нам нужно, чтобы useFetch сохранял данные из эндпоинта и отображал состояние загрузки. Формат данных будет задан отдельно, для этого используем дженерик: useFetch.ts Таким образом, Теперь сама функция: useFetch.ts Для полноты картины добавили Теперь окончательно отрефакторим useBrowsers.ts Сравните с исходным вариантом. Кажется, здесь больше нечего извлекать 🙂 Используйте возможности IDE, ESLint и Prettier. Использование автоматического импорта сэкономит много времени. Разделяя проект на маленькие переиспользуемые кирпичики вы неизбежно приходите к монструозной файловой структуре вроде этой: Привыкните использовать возможности IDE: поисковую строку для поиска компонентов, горячие клавиши для перемещения по истории файлов и всплывающие определения, которые появляются при наведении курсора на название компонентов. Это поможет найти самые хитрые и вредные ошибки: *** Чтобы быть быстрым, не потеряв при этом в качестве, вам нужно освоить несколько вещей: Насколько вы постигли дзен быстрой разработки? Признавайтесь, часто рефакторите? Дополнительные материалы:Учимся готовить компоненты
Исходный компонент
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' // Элемент массива // {"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;
Что с этим компонентом так?
Что с этим компонентом не так?
К чему мы стремимся?
Приступаем к огранке
Шаг #1. Перенос констант в пропсы
function Browsers({url = 'https://google.com/myData.json'}) { const URL = url ...
Шаг #2. Разделение бизнес-логики и отображения
<Browsers/>
до инструкции return
.return
в нашем примере.useBrowsers()
с бизнес-логикой.<BrowsersList />
с логикой отображения.
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
<BrowsersList />
теперь можно использовать с разными источниками данных (а не только с данными из HTTP-эндпоинтов).useBrowsers()
теперь может работать с разными представлениями или даже вообще без представления. Если вы измените дизайн приложения, этот хук продолжит работать.Шаг #3. Разделение на файлы
<Browsers/>
из Browsers.jsx;<Browsers/>
лежат в папках components и hooks;
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> ) }
Большая проблема маленьких файлов
<BrowsersList />
. Что произойдет, если мы переименуем какой-нибудь пропс, например, changeDescription
на setSelectedBrowser
? Или удалим аргумент? Или добавим новый?
... return { loading, browsers, modalVisible, descripton, changeDescription, changeOpacity, } ...
... export function BrowsersList({ loading, browsers, modalVisible, description, changeDescription, changeOpacity, }) { ....
Хватит медленно кодить на JS, давайте кодить быстро на TS
Шаг #4. Определение типов
<BrowsersList />
и преобразуем его пропсы в симпатичные типы (не забудьте сменить расширение с .jsx на .tsx).
// ... export type Browser = { fullname: string // ожидается, что поле "fullname" - это строка 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[] // ожидается, что поле "browsers" это массив элементов с типом Browser modalVisible: boolean description: string // ожидается, что changeDescription - это функция // которая принимает строку и ничего не возвращает changeDescription: (description: string) => void // ожидается, что changeOpacity - это функция // которая ничего не принимает и ничего не возвращает 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) // ...
useBrowsers()
и BrowsersList
были совместимы. Быстрая разработка архитектуры
BrowsersListProps
выглядит весьма нагруженно:
export type BrowsersListProps = { loading: boolean browsers: Browser[] modalVisible: boolean description: string changeDescription: (description: string) => void changeOpacity: () => void }
Browser
:<BrowserList />
, чтобы он мог работать с новой сигнатурой BrowsersListProps
. <BrowserItem />
. Сейчас он принимает множество пропсов, и это явный сигнал для внесения правок. Теперь он будет принимать всего 2 аргумента:
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> ) }
Шаг #5. Извлечение <UIFriendlyList />
<BrowsersList />
. Значит надо ее извлечь. Создадим новый компонент <UIFriendlyList />
и будем использовать его вместо простого <FlatList/ >
.
type UIFriendlyListProps<T> = FlatListProps<T> & {loading?: boolean}
T
– это аргумент типа, или дженерик. То же самое, что arg
в сигнатуре foo(arg)
. Дженерики нужны, если вы хотите создать свой тип из другого типа. Вот здесь есть подробнейшее описание дженериков.&
– это символ пересечения множеств (интерсекция). Тип X = A & B
означает, что X
содержит в себе свойства A
и B
.UIFriendlyListProps
расширяет встроенные класс FlatListProps
из библиотеки React Native и добавляет в него состояние загрузки.
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} /> }
<UIFriendlyList />
– это крутой строительный блок, который очень легко переиспользовать в самых разных ситуациях. Добавьте его в свою коллекцию кирпичиков, чтобы стать еще быстрее.<BrowsersList />
:
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> ) }
<UIFriendlyList />
, который в будущем сэкономит вам кучу времени. Можно пойти еще дальше и выделить, например, в отдельный кирпичик логику Модальное Окно со Списком, но давайте пока остановимся.Шаг #6. Рефакторинг useBrowsers()
useBrowswers()
должен возвращать валидный объект с типом BrowsersListProps:
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, } }
useFetch
.
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
можно будет использовать с любыми типами данных.
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, } }
Alert
при ошибке запроса.useBrowsers()
:
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, } }
4 простых совета для ускорения разработки
1. Никогда не форматируйте код вручную.
2. Никогда не импортируйте модули вручную
3. Научитесь ориентироваться среди множества файлов
4. Используйте линтинг
Искусство быстрой разработки
- 4 views
- 0 Comment
Свежие комментарии