Внедрение зависимостей в React

Внедрение зависимостей в React

От автора: внедрение зависимостей (DI) — это шаблон, в котором компоненты, необходимые для запуска кода, могут быть заменены в горячем режиме. Это означает, что зависимости не жестко запрограммированы в реализации и могут изменяться по мере изменения среды.

Благодаря наследованию, DI — это широко используемый шаблон в объектно-ориентированном программировании (ООП), предназначенный для многократного использования кода в различных объектах и классах. Однако основной причиной использования внедрения зависимостей в React является простое моделирование и тестирование компонентов. В отличие от Angular, DI не является обязательным требованием при работе с React, это скорее удобный инструмент, который можно использовать, когда вы хотите что-то улучшить.

Внедрение зависимости в JavaScript

Чтобы проиллюстрировать принципы DI, представьте модуль npm, который предоставляет следующую функцию:

export const ping = (url) => { return new Promise((res) => { fetch(url) .then(() => res(true)) .catch(() => res(false)) })
}

Использование функции ping в современном браузере будет работать нормально.

import { ping } from "./ping" ping("https://logrocket.com").then((status) => { console.log(status ? "site is up" : "site is down")
})

Но запуск этого кода внутри Node.js вызовет ошибку, потому что fetch не реализована в Node.js. Однако существует множество реализаций fetch и полифиллов для Node.js, которые мы можем использовать. DI позволяет нам превратить fetch в инъекционную зависимость для ping, например:

export const ping = (url, fetch = window.fetch) => { return new Promise((res) => { fetch(url) .then(() => res(true)) .catch(() => res(false)) })
}

От нас не требуется указывать для fetch значение по умолчанию window.fetch. Каждый раз, когда мы используем ping нам не требуется интегрировать fetch, что улучшает опыт разработки. Теперь в Node среде мы можем использовать fetch в сочетании с нашей функцией ping, например:

import fetch from "node-fetch"
import { ping } from "./ping" ping("https://logrocket.com", fetch).then((status) => { console.log(status ? "site is up" : "site is down")
})

Работа с несколькими зависимостями

Если у нас есть несколько зависимостей, невозможно добавлять их как параметры: func (param, dep1, dep2, dep3,…). Вместо этого лучше иметь объект для зависимостей:

const ping = (url, deps) => { const { fetch, log } = { fetch: window.fetch, log: console.log, ...deps } log("ping") return new Promise((res) => { fetch(url) .then(() => res(true)) .catch(() => res(false)) })
} ping("https://logrocket.com", { log(str) { console.log("logging: " + str) }
})

Наш параметр deps будет распространен на объект реализации и переопределит функции, которые он предоставляет. При деструктуризации этого измененного объекта уцелевшие свойства будут использоваться в качестве зависимостей. Используя этот шаблон, мы можем переопределить одну зависимость, но не другие.

Внедрение зависимостей в React

Работая с React, мы активно используем пользовательские хуки для извлечения данных, отслеживания поведения пользователей и выполнения сложных вычислений. Излишне говорить, что мы не хотим (и не можем) запускать эти хуки во всех средах.

Отслеживание посещения страницы во время тестирования приведет к повреждению наших аналитических данных, а получение данных из реальной серверной части приведет к медленному выполнению тестов.

Тестирование — не единственная такая среда. Такие платформы, как Storybook, упрощают документацию и могут обойтись без использования многих хуков и бизнес-логики.

Внедрение зависимости через props

Возьмем, к примеру, следующий компонент:

import { useTrack } from '~/hooks' function Save() { const { track } = useTrack() const handleClick = () => { console.log("saving...") track("saved") } return <button onClick={handleClick}>Save</button>
}

Как упоминалось ранее, следует избегать использования useTrack (и, в более широком смысле, track). Поэтому преобразуем useTrack в зависимость компонента Save через props:

import { useTracker as _useTrack } from '~/hooks' function Save({ useTrack = _useTrack }) { const { track } = useTrack() /* ... */
}

Применяя псевдоним к нашему useTracker, чтобы избежать коллизии имен и используя его в качестве значения по умолчанию для props, мы сохраняем хук в нашем приложении и имеем возможность переопределить его всякий раз, когда возникает необходимость.

Имя _useTracker — одно из многих соглашений об именах: useTrackImpl, useTrackImplementation и useTrackDI — все это широко используемые соглашения при попытке избежать коллизии. Внутри Storybook мы можем переопределить хук как таковой, используя имитацию (mock) реализации.

import Save from "./Save" export default { component: Save, title: "Save"
} const Template = (args) => <Save {...args} />
export const Default = Template.bind({}) Default.args = { useTrack() { return { track() {} } }
}

Использование TypeScript

При работе с TypeScript полезно сообщить другим разработчикам, что внедрения зависимостей props — это именно то, что нужно, и использовать реализацию typeof для сохранения безопасности типов:

function App({ useTrack = _useTrack }: Props) { /* ... */
} interface Props { /** * For testing and storybook only. */ useTrack?: typeof _useTrack
}

Внедрение зависимости через Context API

Благодаря работе с Context API, внедрение зависимостей позволяет почувствовать себя первоклассным пользователем React. Возможность переопределения контекста, в котором наши хуки запускаются на любом уровне компонента, пригодится при переключении сред.

Многие известные библиотеки предоставляют имитированные реализации своих провайдеров с целью тестирования. React Router v5 имеет MemoryRouter, а Apollo Client предоставляет MockedProvider. Но если мы используем подход, основанный на DI, такие подставные провайдеры не нужны.

React Query — яркий тому пример. Мы можем использовать одного и того же поставщика как для разработки, так и для тестирования и передавать его разным клиентам в каждой среде. В процессе разработки мы можем использовать простой queryClient со всеми параметрами по умолчанию без изменений.

import { QueryClient, QueryClientProvider } from "react-query"
import { useUserQuery } from "~/api" const queryClient = new QueryClient() function App() { return ( <QueryClientProvider client={queryClient}> <User /> </QueryClientProvider> )
} function User() { const { data } = useUserQuery() return <p>{JSON.stringify(data)}</p>
}

При тестировании кода такие функции, как retries, re-fetch при фокусировке окна и время кеширования, можно настроить соответствующим образом.

// storybook/preview.js
import { QueryClient, QueryClientProvider } from "react-query" const queryClient = new QueryClient({ queries: { retry: false, cacheTime: Number.POSITIVE_INFINITY }
}) /** @type import('@storybook/addons').DecoratorFunction[] */
export const decorators = [ (Story) => { return ( <QueryClientProvider client={queryClient}> <Story /> </QueryClientProvider> ) },
]

Внедрение зависимостей в React распространяется не только на хуки, но также и на JSX, JSON и все, что мы хотим абстрагировать или изменить при различных обстоятельствах.

Альтернативы внедрению зависимостей

В зависимости от контекста, внедрение зависимостей может быть неподходящим инструментом для работы. Например, в data-fetching хуках, mock лучше использовать с помощью перехватчика (например, MSW) вместо того, чтобы внедрять хуки по всему тестовому коду, а mocking функции остаются громоздким инструментом для решения более серьезных проблем.

Почему вы должны использовать внедрение зависимостей?

Причины использования DI:

Никаких накладных расходов при разработке, тестировании или производстве

Чрезвычайно легко реализовать

Не требует mocking/stubbing библиотеки, потому что они встроены в JavaScript.

Работает для всех ваших потребностей, таких как компоненты, классы и обычные функции.

Причины не использовать DI:

Загромождает ваш импорт и props / API компонентов

Может сбивать с толку других разработчиков

Заключение

В этой статье мы рассмотрели руководство по внедрению зависимостей в JavaScript и обосновали его использование в React для тестирования и документации. Мы использовали Storybook, чтобы проиллюстрировать использование DI, и, наконец, обсудили причины, по которым вы должны и не должны использовать DI в своем коде.

Автор: Simohamed Marhraoui

Источник: blog.logrocket.com

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен