Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Существует токсичный стереотип, что FullStack разработчики не могут ни в фронт, ни в бек. Как минимум, так как объем работ большой, часто, программирование фронта на React превращается в формошлепство с сомнительным качеством кода. Код копируется без создания компонентов, нет глобального состояния приложения.
В этой статье я приведу примеры использования нескольких компонентов, которые позволят малой кровью выкатить фичу на тестовый стенд так, чтобы код, загружающий данные с сервера, легко поддавался рефакторингу в недалеком будущем.
Лучшее состояние UI для FullStack - отсутствие состояния
Так как состояние фронта дублирует структуры данных, объявленные на стороне backend, причиной появления плохого кода скорее всего будет желание сэкономить на их фронтовых описаниях. Как вариант, рискну предложить возродить использование stateless подхода для простых форм React приложений.
Функция createProvider
Позволяет вынести какой-либо объект в React контекст. Вместе с Mobx может быть использована для создания общего controller-а для вложенных view-шек. В методах анонимного класса getPosts
и getPostById
используется функция fetchApi
- алиас для fetch(...).then((data) => data.json())
import { createProvider, fetchApi } from 'react-declarative';
const [BlogApiProvider, useBlogApi] = createProvider(
() => new class {
getPosts = () =>
fetchApi("https://jsonplaceholder.typicode.com/posts")
getPostById = (id) =>
fetchApi(`https://jsonplaceholder.typicode.com/posts/${id}`)
}
);
export { BlogApiProvider, useBlogApi };
Компонент FetchView
Подходит для загрузки данных и отображения списочных форм без пагинации. Поддерживает загрузку данных из нескольких ручек, список указывается пропcой state. После загрузки данных происходит анимация появления списка.
import { FetchView } from 'react-declarative'
const PostList = () => {
const { getPosts } = useBlogApi();
const state = [
getPosts,
];
return (
<FetchView state={state} animation="fadeIn">
{(posts) => (
<div>
{posts.map((post, idx) => (
<p key={idx}>
<b>{post.title}</b>
{post.body}
</p>
))}
</div>
)}
</FetchView>
);
};
Компонент Async
В отличие от FetchView
умеет показывать спиннер (см CircularProgress
), сигнализирующий пользователю о активной загрузке данных. Если id
изменится, то запрос выполнится повторно задействуя пропсу payload
import { Async } from 'react-declarative'
import { CircularProgress } from '@mui/material'
const PostItem = ({
id,
}) => {
const { getPostById } = useBlogApi();
return (
<Async payload={id} Loader={CircularProgress}>
{async (id) => {
const { title, body } = await getPostById(id);
return (
<div>
<p>{title}</p>
<p>{body}</p>
</div>
);
}}
</Async>
);
};
Компонент Switch
Роутер, с использованием которого легко настроить кеширование и инвалидацию кеша. Для примера, реализуем ролевую модель.
import { createProvider, fetchApi, singleshot } from 'react-declarative';
import sleep from '../utils/sleep';
const roleApiManager = new class {
getRoles = singleshot(async () => {
await sleep(5_000);
return [
'admin',
...
]
});
hasRole = async (role) => {
const roles = await this.getRoles();
return roles.includes(role);
};
unload = () => {
this.getRoles.clear();
};
};
const [RoleApiProvider, useRoleApi] = createProvider(
() => roleApiManager
);
export { roleApiManager, RoleApiProvider, useRoleApi };
На странице'/sample-page'
список ролей может быть потребован в нескольких участках кода, но запрос должен выполниться только один раз. После того, как пользователь покинет страницу, кешированный список ролей должен сброситься.
import { Switch } from 'react-declarative';
...
const routes = [
{
path: '/sample-page',
unload: roleApiManager.unload,
},
];
...
const App = () => (
<Switch history={history} items={routes} />
);
Компонент If
Этот компонент осуществит ветвление представления исходя из истинности промиса в condition
. Он так же умеет перезапрашивать данные при изменении payload
import { If } from 'react-declarative'
const ProfilePage = () => {
const { hasRole } = useRoleApi();
return (
<If condition={() => hasRole("admin")}>
<button>Кнопка только для админа</button>
</If>
);
};
Краткость сестра таланта
Если данные компоненты вызовут интерес среди сообщества, я напишу цикл статей, посвященной упрощению рутины на стороне фронтенда.