Конкурс инструкций

Оптимизация подключения REST API для React приложений

Основная идея

Инструкция направлена на оптимизацию способа обмена данными между клиентом и сервером.

При написании компонентов, большинство разработчиков нагружают свой код излишней логикой в процессе подключения REST API. В итоге компонент в большей степени выполняет роль прокси объекта, и становится тяжело читаемым, трудно тестируемым, что особо пагубно влияет на разработку в команде. Основная задача инструкции направлена на обеспечение более декларативного подхода. Ниже представлены два варианта: код оптимального подключения REST API и классический способ для одного и того же компонента.

//OPTIMAL METHOD const CartList = () => { const {cart} = useSelector((state) => state) const clear = () => { API.clear() .catch((e)=>alert(e)) } return <View> {cart.map(({productName}, key) => { return <Text>{productName}</Text> })} <Button title={'clear'} onPress={clear}/> </View> } //CLASSIC OLD const CartList = () => { const dispatch = useDispatch() const {cart} = useSelector((state) => state) const clear = () => { //Request -> response -> logic -> dispatch fetch('http://server.com' + '/clear') .then(res => res.json()) .then(({result, cart}) => { result ? dispatch(setCart(cart)) : alert('FAIL') }) } return <View> {cart.map(({productName}, key) => { return <Text>{productName}</Text> })} <Button title={'clear'} onPress={clear}/> </View> }

В первом случае мы используем уникальный объект «API», который сам выполняет все за нас, обеспечивает более полиморфный подход и переносит логику в общий модуль. Во втором примере присутствует большая логическая нагрузка на компонент.

Далее будет рассмотрена реализация данного объекта с использованием redux и нативных возможностей javascript.
Информация актуальна для React и React-Native приложений.

Однако существует множество других шаблонов обмена бизнес данными между клиентом и сервером с использованием сторонних библиотек. Некоторые из них:

Оглавление

Интеграция клиентского API

1. Создание класса API.

Предполагается, что на данном этапе уже существуют конкретные эндпоинты для запроса на сервер.
Требуется создать отдельный модуль с названием API.
Он будет содержать конструктор класса, экспорт его экземпляра, методы для взаимодействия с сервером и состоянием приложения.
Пример:

//API create class API{ // ... methods ... } export default new API()

Далее требуется наполнить класс асинхронными методами для взаимодействия с сервером.

//API method add class API{ location = 'http://server.com/' //GET /list async getCartList(){ return await fetch(this.location + 'list').then(res=>res.json()) } //POST /:id async addProduct({product_id}){ return await fetch(this.location + product_id, {method:'post'}).then(res=>res.json()) } } export default new API()

В данном примере добавлены два метода «getCartList» (получение списка данных) и «addProduct» (добавление данных на сервер). В результате вызова оба метода возвращают Promise, которые при выполнении возвращает ответ сервера в JSON формате.

2. Интеграция Redux.

Далее требуется добавить взаимодействие с состоянием приложения через redux store. Предполагается, что уже подключен redux \ modx (В примере redux). Добавим dispatch метод, и будем его вызывать внутри наших методов (можно вызвать dispatch до запроса для мгновенного обновления состояния, либо после запроса, когда требуется использовать данные из ответа сервера.)

//dispatching in API import store from './stroe' import {setCartAction, addProductById} from './actions' class API{ location = 'http://server.com/' dispatch(action){ store.dispatch(action) } //GET /list async getCartList(){ const result = await fetch(this.location+'list').then(res=>res.json()) //dispatch after request this.dispatch(setCartAction(result)) return result } //POST /:id async addProduct({product_id}){ //dispatch before request this.dispatch(addProductById(product_id)) return await fetch(this.location + product_id, {method:'post'}).then(res=>res.json()) } } export default new API()
В результате получается довольно читабельный и последовательный алгоритм (на схеме справа) [email protected]

3. Использование внутри компонентов.

В результате мы можем использовать наш класс внутри компонентов множества компонентов. При модификации api запроса потребуется изменить лишь логику внутри одного из методов класса внутри нашего модуля.
Пример использования:

const ProductAdd = () => { const {cart} = useSelector((state) => state) const addProduct = (product_id) => { API.addProduct(product_id) .then(({data})=>{ // *** success *** }) .catch((e)=>alert(e)) } return <View> {cart.map(({product_name, product_id}, key) => { return <Text onPress={()=>addProduct(product_id)} > {product_name} </Text> })} </View> } export default ProductAdd;
Благодаря такому подходу контекст тела компонента становится чище и лучше, подобно тому, как мы создаем собственные хуки для переноса логики.  [email protected]

При этом мы можем вызывать методы класса с использованием своих хуков, например:

// file myHooks.js import {useSelector} from 'react-redux'; import API from './API' export const useUserDataRequest = ()=>{ const {user} = useSelector() useEffect(()=>{ API.getAndSetUserData() },[]) return {user} } export default useUserDataRequest; ////////////////////// //View component file import {useUserDataRequest} from "./myHooks" export default ()=>{ const {user} = useUserDataRequest() if(!user){ return <p>Загрузка ...</p> } return <h1>{user.name}</h1> }

В момент отрисовки компонента, происходит вызов метода API и происходи загрузка данных и смена состояния. Данный подход очень напоминает хуки graphql-apollo.

Идеология

Благодаря данному подходу мы избавляемся от большой логической нагрузки и уменьшаем зону ответственности при подключении и модификации api.
Сплошными стрелками на схеме отмечена основная логическая нагрузка при подключении.

Без использования API класса [email protected]
С использованием API класса становится намного меньше логики которую нужно брать во внимание при написании компонента. [email protected]

Axios

Данный паттерн можно модифицировать благодаря использованию библиотеки axios .
Основная польза от использования библиотеки:

  • Доступ к Interceptors (перехват запросов и ответов через обработчик)
  • create метод для создания универсального интерфейса запросов.
  • Оптимизация обработки ответа сервера для text и json формата.
  • Удобная типизация.

Пример класса API с использованием основных кейсов библиотеки

//axios API pattern const server = axios.create({ baseURL: 'http://server.com', }) class API { constructor() { //INITIAL REQ AND RES OBSERVE server.interceptors.request.use((req) => { this.dispatch(setLoadingAction(true)) return req }) server.interceptors.response.use((res) => { this.dispatch(setLoadingAction(false)) return res }) } dispatch(action){ store.dispatch(action) } getHeaderWithToken(){ return {headers: {token: store.getState().token}} } setError(e){ console.warn(e) dispatch(setErrorAction(e)) } async getUserDataAndSet(){ const result = await server.get( '/user/info', this.getHeaderWithToken()) .catch((e) => this.setError(e)) this.dispatch(setUserAction(result.data.user)) return result } }
axios предоставляет удобный интерфейс для перехвата события запроса и\или ответа. При этом можно модифицировать объекты данных. Это удобно, когда требуется изменить состояние загрузки приложения внутри redux при каждом запросе. [email protected]

Typescript

Для защиты типов и поддержки кода добавим типизацию с применением typescript. Такой подход позволит получить доступ к результатам ответа сервера без документации к API сервера спустя длительное время работы с кодом.

import {setLoadAction, setProductCartAction, setTokenAction, setUserAction} from "../store/actions"; import {ActionType, ProductType, StateType, TokenType, UserType} from "../types/types"; import axios, {AxiosRequestConfig, AxiosResponse} from 'axios' import store from "../store/store"; import LOCATION from "../vars/LOCATION"; import Message from "../config/Message"; import formatPhone from "../config/formatPhone"; const server = axios.create({ baseURL: LOCATION, }) type IResult<T = {}> = { data: { result: boolean } & T } interface IStatus { status?: number } type SuccessResponse<T = {}> = IResult<T> & IStatus type FailRespone = IResult //API AUTOMATICLY SET STORE APPLICATION STATE class API { constructor() { //INITIAL REQ AND RES SUBSCRIBE OBSERVE server.interceptors.request.use((req: AxiosRequestConfig) => { this.setLoader(true) if (req?.data?.phone) { const {phone} = req.data req.data.phone = formatPhone(phone) } return req }) server.interceptors.response.use((res: AxiosResponse) => { this.setLoader(false) return res }) } private dispatch(action: ActionType) { store.dispatch<ActionType>(action) } private FailResult: FailRespone = {data: {result: false}} private getState(): StateType { return store.getState() } private getUserToken(): TokenType { return this.getState().token || '' } private setLoader(load: boolean) { this.dispatch(setLoadAction(load)) return load } private setUserTokenToStore({token}: { token: TokenType }): void { this.dispatch(setTokenAction(token)) } private setError(e: Error): IResult { this.setLoader(false) Message('Ошибка соединения') console.warn(e, 'ERR') return this.FailResult } private getHeaderWithToken(): { headers: { token: TokenType } } { return {headers: {token: this.getUserToken()}} } async getSmsCodeByPhone({phone}: { phone: string }): Promise<SuccessResponse> { return (await server.post<{ phone: string }, any>('/user/code', {phone}) .catch((e) => this.setError(e))) } async verifyUserLoginByCodeAndSetToken({phone, code}: { phone: string, code: string }): Promise<SuccessResponse<{ is_registered?: boolean, token?: string }>> { const result: SuccessResponse<{ is_registered?: boolean, token?: string }> = (await server.post<{ phone: string, code: string }, any>('/user/verify', {phone, code}) .catch((e) => this.setError(e))) if (result?.data?.token) { this.dispatch(setTokenAction(result.data.token)) } return result } async registrationUserIfToken({username, email, phone}: { username: string, email: string, phone: string }): Promise<SuccessResponse> { if (!this.getUserToken()) { console.warn('TOKEN NOT EXIST IN STORE') return this.FailResult } return (await server.post<{ username: string, email: string, phone: string }, any>( '/user/registration', {username, email, phone}, this.getHeaderWithToken()) .catch((e) => this.setError(e))) } async scanProductByScanner({code}: { code: string }): Promise<SuccessResponse<{ exists?: boolean, product?: ProductType, cart?: ProductType[], total?: string }>> { const result: SuccessResponse<{ exists?: boolean, product?: ProductType, cart?: ProductType[], total?: string }> = (await server.post<{ code: string }, any>( '/product/add', {code}, this.getHeaderWithToken()) .catch((e) => this.setError(e))) if (result?.data?.result && result?.data?.cart && result?.data?.total) { this.dispatch(setProductCartAction(result.data.cart, result.data.total)) return result } console.warn('CART NONE OR RESULT FALSE') return this.FailResult } } export default new API()

Контакты

Если у вас возникли вопросы, буду рад ответить и обсудить

Турченко Степан
Fullstack middle developer
Как вы общаетесь с сервером в своих компонентах
current API class pattern
graphql-apollo
redux-saga / thunk
другое
Показать результаты
Переголосовать
Проголосовать
0
6 комментариев
Написать комментарий...
Saucedo Puetz

Степ, пости такое лучше на хабр, тутошний народ гораздо больше любит когда кого-то Яндекс еда обманула на гамбургер

Ответить
Развернуть ветку
Андрей Малинин

Я извиняюсь как человек, пишущий в ооп стиле и пользующийся мобиксом, вот этот трэш, подписанный как "CLASSIC OLD", его реально пишут? Его там тимлиды смотрят на код ревью, говорят что все норм и аппрувят? Просто мне казалось, в редаксе запросы к сервисам принято хотя бы в экшены прятать.

Ответить
Развернуть ветку
Степан Турченко
Автор

привет,
Пример с использованием экшенов я особо не упоминал, тк это в большей степени напоминает сагу, где происходит практически то же самое , но через прослойку эффектов.
Если посмотреть ≈90% гайдов с ютуба, все они рекомендуют именно такой способ "classic".
Сам я рекомендую использовать данный способ из статьи только при не большом количестве эндпоинтов, когда выгоднее отказаться от включения библиотеки

Ответить
Развернуть ветку
Степан Турченко
Автор

Привет, дай наводку, где мы с тобой виделись)
Возможно, это мой первый опыт, 
появился повод попробовать для конкурса

Ответить
Развернуть ветку
Степан Турченко
Автор

🖐️

Ответить
Развернуть ветку
О. Войтенко

А есть пример проекта на GitHub? Хотелось бы посмотреть это в действии.

Ответить
Развернуть ветку
3 комментария
Раскрывать всегда