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

stepanstepan4@gmail.com
stepanstepan4@gmail.com

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

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

При написании компонентов, большинство разработчиков нагружают свой код излишней логикой в процессе подключения 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 приложений.

stepanturchenko@mail.ru
stepanturchenko@mail.ru

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

Оглавление

Интеграция клиентского 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()
В результате получается довольно читабельный и последовательный алгоритм (на схеме справа) stepanstepan4@gmail.com
В результате получается довольно читабельный и последовательный алгоритм (на схеме справа) stepanstepan4@gmail.com

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;
Благодаря такому подходу контекст тела компонента становится чище и лучше, подобно тому, как мы создаем собственные хуки для переноса логики.  stepanstepan4@gmail.com
Благодаря такому подходу контекст тела компонента становится чище и лучше, подобно тому, как мы создаем собственные хуки для переноса логики.  stepanstepan4@gmail.com

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

// 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 класса stepanstepan4@gmail.com
Без использования API класса stepanstepan4@gmail.com
С использованием API класса становится намного меньше логики которую нужно брать во внимание при написании компонента. stepanstepan4@gmail.com
С использованием API класса становится намного меньше логики которую нужно брать во внимание при написании компонента. stepanstepan4@gmail.com

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 при каждом запросе. stepanstepan4@gmail.com
axios предоставляет удобный интерфейс для перехвата события запроса и\или ответа. При этом можно модифицировать объекты данных. Это удобно, когда требуется изменить состояние загрузки приложения внутри redux при каждом запросе. stepanstepan4@gmail.com

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
  • stepanturchenko@mail.ru
  • stepanstepan4@gmail.com
Как вы общаетесь с сервером в своих компонентах
current API class pattern
graphql-apollo
redux-saga / thunk
другое
1313
6 комментариев

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

5

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

1

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

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

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