Как стать автором
Обновить

CRUD React c Redux и Saga + typescript в 2023

Уровень сложностиСредний
Время на прочтение15 мин
Количество просмотров7.5K
это типа суп
это типа суп

Статья посвящается конкретно Redux+Saga+Typescript. Практика, которую я видел в разных коммерческих проектах, и с которой до сих пор сталкиваюсь. redux + saga уже является дедовским методом ( из за большой шаблонности кода - бойлерплейта ) , потому что сейчас актуальнее redux-toolkit с RTK query или effector, кому то может и mobx :) . Следующим постом я бы хотел переписать имеющийся код на FSD архитектуру с redux-toolkit + RTK query и разобрать отличия.

Предисловие

В этой статье будет представлена классическая архитектура, и на ее примере я хотел бы коротко но ясно объяснить работу redux , saga и их типизацию. Если кому то будет интересно еще немного углубиться в детали - мне понравилась вот эта статья.

Возможно кому то будет удобнее еще большая декомпозиция, проектик будет лежать тут ( и да, просто для скорости создал на CRA, а если кто то будет использовать скелет, то лучше сделать на vite например )

Примером послужит код под CRUD совсем небольшого выдуманного блога.

Идея в том, что на одной страничке мы создадим форму для создания/редактирования постов ( во всплывающем окне ) и добавим возможность их удаления.

мок API я возьму с https://jsonplaceholder.typicode.com.

Оглавление

  1. Создаем константы

  2. Создаем actions

  3. Создаем reducer

  4. Создаем saga

  5. Регистрируем в store.

  6. О запросах

  7. О типизации

  8. Вкратце про компоненты

Вступление

В контексте статьи у redux существует несколько составляющих :

  • State (Состояние): В основе Redux лежит глобальный объект состояния, который служит единым хранилищем для всего приложения. Этот объект хранит свойства состояний различных сущностей, обеспечивая централизованное управление и упрощение отслеживания изменений состояний.

  • Actions (Действия): Действия представляют собой функции - описания возможных событий или манипуляций, которые могут произойти в систем. Они запускаются в ответ на взаимодействия пользователя или системные события и передают данные в редюсеры и саги для обработки.

  • Reducers (Редюсеры): Редюсеры — это чистые функции, которые принимают предыдущее состояние и действие как аргументы и возвращают новое состояние. Они следуют принципу неизменности, создавая новое состояние вместо изменения существующего.

  • Sagas (Саги): Саги - функции генераторы, представляют собой инструмент для управления побочными эффектами, такими как асинхронные операции и доступ к системным ресурсам. Саги нужны для обработки асинхронных потоков управления в манере, которая выглядит как синхронный код.

  • Effects (Эффекты): Эффекты в сагах служат для описания различных асинхронных операций. Они представляют собой объекты, которые описывают типы побочных эффектов, которые должны быть выполнены сагой.

  • Store (Хранилище): Хранилище в Redux служит как центральный регистратор для всех редюсеров и саг в приложении. Оно создает глобальное состояние приложения, обеспечивая доступ ко всем функциональным возможностям управления состоянием.

Побочные эффекты — когда функция влияет на глобальное состояние или внешнюю среду. ( изменение аргументов функции напрямую, другими словами мутация, изменение DOM дерева и т.п )

Давайте рассмотрим подробнее ( с примерами ) ниже.

Включаемся в код

1. Константы для действий

Начнем с констант для типов в actions.

Здесь я выношу их в отдельный файлик для декомпозиции. Cам подход их написания дает возможность исключать опечатки и впоследствии безболезненно изменять эти константы. Например, если у нас появится версионность:

const CREATE_POST_REQUEST = "CREATE_POST_REQUEST";

переходит в следующее:

const CREATE_POST_REQUEST = "CREATE_POST_REQUEST_V1";

const CREATE_POST_REQUEST_V2 = "CREATE_POST_REQUEST_V2";

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

К слову - actions у нас под каждое событие - сам запрос, и два исхода в ответе
( success,failure ). Это нужно для последующей работы с состоянием в редюсере.

src/consts/posts/PostsConsts.ts
export const FETCH_POSTS_REQUEST = 'FETCH_POSTS_REQUEST';
export const FETCH_POSTS_SUCCESS = 'FETCH_POSTS_SUCCESS';
export const FETCH_POSTS_FAILURE = 'FETCH_POSTS_FAILURE';

export const CREATE_POST_REQUEST = 'CREATE_POST_REQUEST';
export const CREATE_POST_SUCCESS = 'CREATE_POST_SUCCESS';
export const CREATE_POST_FAILURE = 'CREATE_POST_FAILURE';

export const UPDATE_POST_REQUEST = 'UPDATE_POST_REQUEST';
export const UPDATE_POST_SUCCESS = 'UPDATE_POST_SUCCESS';
export const UPDATE_POST_FAILURE = 'UPDATE_POST_FAILURE';

export const DELETE_POST_REQUEST = 'DELETE_POST_REQUEST';
export const DELETE_POST_SUCCESS = 'DELETE_POST_SUCCESS';
export const DELETE_POST_FAILURE = 'DELETE_POST_FAILURE';

2. Actions

Здесь представлены Функции-Создатели Действий ( Action Creators ): Эти функции возвращают объект действия, который содержит тип действия ( как раз наши константы выше ) и данные, которые должны быть переданы в редюсер для обновления состояния. Их мы будем дергать в компонентах, а обрабатывать в редюсерах и сагах.

Сам шаблон функции экшн креэйтора в данном приложении - состоит из следующих сегментов:

  • Название - { тип запроса }{ имя сущности }{ запрос или варианты ответа }.

  • Аргумент - обычно его называют data и типизируют общим ожидаемым типом. А сам тип пишется также как сегмент названия, только в другом case и с добавочным словом Interface/Type.

  • Возвращаемое значение - тоже тип, пишется как и тип значения аргумента, только туда прибавляется слово Action для конкретизации , что должен вернуться именно объект действия.

  • Объект действия - обычно имеет два свойства - type и payload, в первое кладется константа типа действия, во второй название принимаемого функцией аргумента (data).

При написании экшн креэйторов - берется шаблон и просто клонируется с изменением этих сегментов.

src/redux/actions/postsActions.ts
export const updatePostRequest = (data: UpdatePostRequestInterface): UpdatePostRequestActionInterface => ({
  type: UPDATE_POST_REQUEST,
  payload: data
});

export const updatePostSuccess = (data: UpdatePostSuccessResponseInterface): UpdatePostSuccessActionInterface => ({
  type: UPDATE_POST_SUCCESS,
  payload: data
});

export const updatePostFailure = (error: string): UpdatePostFailureActionInterface => ({
  type: UPDATE_POST_FAILURE,
  payload: {
    message: error
  },
});

export const createPostRequest = (data: CreatePostRequestInterface): CreatePostRequestActionInterface => ({
  type: CREATE_POST_REQUEST,
  payload: data
});

export const createPostSuccess = (data: CreatePostSuccessResponseInterface): CreatePostSuccessActionInterface => ({
  type: CREATE_POST_SUCCESS,
  payload: data
});

export const createPostFailure = (error: string): CreatePostFailureActionInterface => ({
  type: CREATE_POST_FAILURE,
  payload: {
    message: error
  },
});

export const deletePostRequest = (data: DeletePostRequestInterface): DeletePostRequestActionInterface => ({
  type: DELETE_POST_REQUEST,
  payload: data
});

export const deletePostSuccess = (data: {}): DeletePostSuccessActionInterface => ({
  type: DELETE_POST_SUCCESS,
  payload: data
});

export const deletePostFailure = (error: string): DeletePostFailureActionInterface => ({
  type: DELETE_POST_FAILURE,
  payload: {
    message: error
  },
});

export const fetchPostsRequest = (): FetchPostsRequestActionInterface => ({
  type: FETCH_POSTS_REQUEST,
});

export const fetchPostsSuccess = (data: FetchPostsSuccessResponseInterface): FetchPostsSuccessActionInterface => ({
  type: FETCH_POSTS_SUCCESS,
  payload: data,
});

export const fetchPostsFailure = (error: string): FetchPostsFailureActionInterface => ({
  type: FETCH_POSTS_FAILURE,
  payload: {
    message: error
  },
});

Когда actions вызывается в компоненте - она создает объект действия, который затем передается в Redux store через dispatch метод. Это начальная точка в цикле управления состоянием приложения. В концепции redux dispatch - это функция, которая принимает action, передает его редюсеру, получает от него новый объект и посылает сигнал всем, кто подписался на изменение состояния.

dispatch(updatePostRequest(data));

Каким образом они вызываются в компоненте вы можете глянуть например тут

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

3. Reducers

Редюсеры в этой механике Redux играют роль "управляющих" состоянием сущностей в глобальном корневом состоянии.

Некоторые фишки редюсеров:

  1. Иммутабельность: Редюсеры предполагают иммутабельность, что обеспечивает предсказуемость происходящего в коде. Иммутабельность означает, что объект или структура данных не может быть изменена после своего созданиях.

  2. Switch Statements: Редюсеры предполагают конструкцию switch для обработки различных типов действий - это позволяет явно управлять тем, как состояние обновляется в ответ на каждое действие.

  3. Контракт Редюсера: Редюсер должен быть чистой функцией, что означает, что при одинаковых входных данных он всегда должен возвращать одинаковый вывод, и не должен иметь побочных эффектов (внутрь него не надо класть доп запросы или работу с дом деревом) .

В каждом файле с редюсером обычно для наглядности сверху лежит initial состояние с дефолтными значениями свойств, которое редюсер принимает как первый аргумент и будет с ним работать внутри, вторым аргументом он ожидает как раз объект действия - action. Внутри редюсера находится свитч кейс по типам действий, и в зависимости от типа - он указывает значения для параметров состояния.

src/redux/reducers/postsReducer.ts
const initialState: PostsState = {
  posts: [],
  successMessage: null,
  isLoading: false,
  error: null,
};

export const postsReducer = (state = initialState, action: PostsActions) => {
  switch (action.type) {
    case FETCH_POSTS_REQUEST:
      return {
        ...state,
        isLoading: true,
        successMessage: null,
        error: null,
      };
    case FETCH_POSTS_SUCCESS:
      return {
        ...state,
        isLoading: false,
        posts: action.payload
      };
    case FETCH_POSTS_FAILURE:
      return {
        ...state,
        isLoading: false,
        error: action.payload.message
      };
    case CREATE_POST_REQUEST:
      return {
        ...state,
        isLoading: true,
        successMessage: null,
        error: null,
      };
    case CREATE_POST_SUCCESS:
      return {
        ...state,
        isLoading: false,
        posts: [ action.payload, ...state.posts ],
        successMessage: 'Post created!'
      };
    case CREATE_POST_FAILURE:
      return {
        ...state,
        isLoading: false,
        successMessage: null,
        error: 'Post was not created',
      };
    case UPDATE_POST_REQUEST:
      return {
        ...state,
        isLoading: true,
        successMessage: null,
        error: null,
      };
    case UPDATE_POST_SUCCESS:
      return {
        ...state,
        isLoading: false,
        posts: state.posts.map(p => p.id === action.payload.id ? action.payload : p) as PostType[],
        successMessage: 'Post updated!',
        error: null
      };
    case UPDATE_POST_FAILURE:
      return {
        ...state,
        isLoading: false,
        successMessage: null,
        error: 'Post was not updated',
      };
    case DELETE_POST_REQUEST:
      return {
        ...state,
        isLoading: true,
        successMessage: null,
        error: null,
      };
    case DELETE_POST_SUCCESS:
      return {
        ...state,
        isLoading: false,
        successMessage: 'Post deleted',
        error: null,
      };
    case DELETE_POST_FAILURE:
      return {
        ...state,
        isLoading: false,
        successMessage: null,
        error: 'Post was not deleted',
      };
    default:
      return state;
  }
};

export default postsReducer;

Помимо postsReducer у нас существует корневой редюсер. ( по сути надо было начинать с него, но для последовательности информации я пишу это немного позже ) Обычно корневой редюсер лежит в index.ts. Его мы будем регистрировать в store, который используется в провайдере для приложения.

src/redux/reducers/index.ts
export interface RootState {
  posts: PostsState;
}

const rootReducer = combineReducers<RootState>({
  posts: postsReducer
});

export default rootReducer;

Для удобства я оставляю интерфейс RootState сверху.

Функция combineReducers принимает объект, где ключи представляют различные срезы состояния, а значения — соответствующие редюсеры.

Вместе с Redux-Saga редюсеры работают "рука об руку" с сагами для обработки асинхронных действий. Редюсеры также обрабатывают действия, которые отправляются сагами после выполнения сайд-эффектов ( побочных эффектов ), для обновления состояния соответствующим образом.

4. Sagas

Сага - это некий слой, играющий свою роль с момента диспатча экшена и до обработки его редюсером. Саги являются функциями генераторами.

В рамках функций-генераторов, используется ключевое слово yield ( по типу return, но return в функции генераторе ) для ожидания завершения асинхронных операций, прежде чем продолжить выполнение кода.

В слое саг существует такая терминология, как саги-наблюдатели и саги-обработчики.

  • Саги-обработчики принимают action и производят некоторые побочные эффекты, в нашем случае вызов API. После выполнения асинхронных операций, обработчики могут вызывать новые actions, чтобы обновить состояние приложения соответственно.

  • Саги-наблюдатели, с другой стороны, прослушивают определенные типы экшенов, запуская соответствующие саги-обработчики при обнаружении этих экшенов.

src/redux/sagas/postsSaga.ts
function* updatePostWorker(action: Action<typeof UPDATE_POST_REQUEST> & UpdatePostRequestActionInterface): Generator {
  try {
    const data = action.payload;
    const response = (yield call(updatePostRequest, data)) as UpdatePostSuccessResponseInterface;
    yield put(updatePostSuccess(response));
    // yield put(fetchPostsRequest());
  } catch (error) {
    yield put(updatePostFailure('error update post'));
  }
}

function* createPostWorker(action: Action<typeof CREATE_POST_REQUEST> & CreatePostRequestActionInterface): Generator {
  try {
    const data = action.payload;
    const response = (yield call(createPostRequest, data)) as CreatePostSuccessResponseInterface;
    yield put(createPostSuccess(response));
    // yield put(fetchPostsRequest());
  } catch (error) {
    yield put(createPostFailure('error create post'));
  }
}

function* deletePostWorker(action: Action<typeof DELETE_POST_REQUEST> & DeletePostRequestActionInterface): Generator {
  try {
    const { id } = action.payload;
    const response = (yield call(deletePostRequest, id)) as {};
    yield put(deletePostSuccess(response));
    yield put(fetchPostsRequest());
  } catch (error) {
    yield put(deletePostFailure('error delete post'));
  }
}

function* fetchPostsWorker(action: Action<typeof FETCH_POSTS_REQUEST> & FetchPostsRequestActionInterface): Generator {
  try {
    const response = (yield call(getPostsRequest)) as FetchPostsSuccessResponseInterface;
    yield put(fetchPostsSuccess(response));
  } catch (error) {
    yield put(fetchPostsFailure('error fetch posts'));
  }
}

export function* postsSagaWatcher(): Generator {
  yield takeLatest(FETCH_POSTS_REQUEST, fetchPostsWorker);
  yield takeLatest(DELETE_POST_REQUEST, deletePostWorker);
  yield takeLatest(CREATE_POST_REQUEST, createPostWorker);
  yield takeLatest(UPDATE_POST_REQUEST, updatePostWorker);
}

call эффект, который используется для вызова функции.

put эффект используется для отправки (dispatch) действий в Redux store.

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

src/redux/sagas/index.ts
function* rootSaga() {
  yield all([ fork(postsSagaWatcher) ]);
}

export default rootSaga;

функция fork используется для создания неблокирующих вызовов, то есть postsSagaWatcher будет запущена, но не будет блокировать выполнение других саг или эффектов, запущенных через all.

5. Store

Store здесь служит фундаментальным регистратором, который используется для создания хранилища Redux, центрального места для хранения состояния вашего приложения.

src/redux/store/index.ts
const sagaMiddleware = createSagaMiddleware();

const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
  rootReducer,
  composeEnhancers(applyMiddleware(sagaMiddleware))
);

sagaMiddleware.run(rootSaga);

export default store;

sagaMiddleware: создает экземпляр middleware саги, используя функцию createSagaMiddleware. Впоследствии он запускается через run и начинает отслеживать действия.

Enhancers - это усилители хранилища, в контексте данной практики усилителем является middleware, но также есть и другие усилители. (вот кстати старенькая статья об этом и как что под капотом).


applyMiddleware принимает массив middleware функций, а возвращает функцию усилитель (в коде createStore она была параметром enhance).

Этот store мы в итоге прокидываем через провайдер в наше приложение. Он и является всем супом хранилища.

src/index.tsx
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

6. О запросах

Для запросов, которые вызываются в saga через call эффект я использовал Axios.

src/api/posts
const postsApi = axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com/',
});

export const getPostsRequest = async () => {
  const response = await postsApi.get('posts');
  return response.data;
}

export const deletePostRequest = async (id: number) => {
  const response = await postsApi.delete(`posts/${ id }`);
  return response.data;
}

export const createPostRequest = async (data: CreatePostRequestInterface) => {
  const response = await postsApi.post(`/posts`, data, {
    headers: {
      'Content-Type': 'application/json'
    }
  });
  return response.data;
}

export const updatePostRequest = async (data: UpdatePostRequestInterface) => {
  const response = await postsApi.put(`/posts/${ data.postId }`, data, {
    headers: {
      'Content-Type': 'application/json'
    }
  });
  return response.data;
}

в первых строках создается экземпляр со своей конфигурацией, вместо того, чтобы к каждому запросу дублировать url.

далее создаются и экспортируются запросы

  1. async: используется перед объявлением функции, которое указывает, что функция является асинхронной. Асинхронные функции всегда возвращают Promise, даже если внутри них нет явного возврата промиса.

  2. await: используется внутри асинхронной функции и оно "приостанавливает" выполнение функции до тех пор, пока промис (в данном случае результат работы postsApi.get('posts')) не будет выполнен. Когда промис завершается, await возвращает результат промиса, и выполнение функции продолжается

7. О типизации

В данном случае к итак существующему redux бойлерплейту добавляется бойлерплейт с типами. Так или иначе - к нему необходимо тоже подходить с умом, называть так, чтобы потом не путаться.

Для текущего круда я привел такую практику:

1) Абстрактный интерфейс операции.

Его я использую для наследования запросов под update и create, т.к они содержат эти же данные. и возможно он пригодится для наследования в интерфейсах под остальные кастомные реквесты.

interfaces/PostOperationInterface.ts
export interface PostOperationInterface {
  userId: number;
  title: string;
  body: string;
}

2) Интерфейсы под круд реквесты. Поместил в один файлик. а если вдруг появятся какие то отдельные кастомные запросы, я бы их декомпозировал в другой в зависимости от контекста.

interfaces/PostsCrudRequests.ts
export interface CreatePostRequestInterface extends PostOperationInterface {
}

export interface DeletePostRequestInterface {
  id: number;
}

export interface UpdatePostRequestInterface extends PostOperationInterface {
  postId: number;
}

3) Интерфейс под PostType. Его я использую как отдельный тип для принятия в респонсах и состоянии постов.

interfaces/PostType.ts
export interface PostType {
  id: number;
  userId: number;
  title: string;
  body: string;
}

4) Интерфейсы под круд респонсы. Думаю все говорит само за себя.

interfaces/PostCrudResponses.ts
export interface FetchPostsSuccessResponseInterface extends Array<PostType> {
}

export interface PostsFailureResponseInterface {
  message: string;
}

export interface CreatePostSuccessResponseInterface extends PostType {
}

export interface UpdatePostSuccessResponseInterface extends PostType {
}

5) Ну и разумеется сами шаблоны типизации в отдельном файле под actions с использованием интерфейсов которые я описал пунктами выше. В конце импортируется интерфейс PostsActions, который мы используем аргументом в редюсере. Через разделитель он перечисляет какие могут туда провалиться actions по типам

interfaces/PostsActions.ts
// Get
export interface FetchPostsRequestActionInterface extends Action<typeof FETCH_POSTS_REQUEST> {
  type: typeof FETCH_POSTS_REQUEST;
}

export interface FetchPostsSuccessActionInterface extends Action<typeof FETCH_POSTS_SUCCESS> {
  type: typeof FETCH_POSTS_SUCCESS;
  payload: FetchPostsSuccessResponseInterface;
}

export interface FetchPostsFailureActionInterface extends Action<typeof FETCH_POSTS_FAILURE> {
  type: typeof FETCH_POSTS_FAILURE;
  payload: PostsFailureResponseInterface;
}

//Delete
export interface DeletePostRequestActionInterface extends Action<typeof DELETE_POST_REQUEST> {
  type: typeof DELETE_POST_REQUEST;
  payload: DeletePostRequestInterface
}

export interface DeletePostSuccessActionInterface extends Action<typeof DELETE_POST_SUCCESS> {
  type: typeof DELETE_POST_SUCCESS;
  payload: {}
}

export interface DeletePostFailureActionInterface extends Action<typeof DELETE_POST_FAILURE> {
  type: typeof DELETE_POST_FAILURE;
  payload: PostsFailureResponseInterface
}

//Create
export interface CreatePostRequestActionInterface extends Action<typeof CREATE_POST_REQUEST> {
  type: typeof CREATE_POST_REQUEST;
  payload: CreatePostRequestInterface
}

export interface CreatePostSuccessActionInterface extends Action<typeof CREATE_POST_SUCCESS> {
  type: typeof CREATE_POST_SUCCESS;
  payload: CreatePostSuccessResponseInterface
}

export interface CreatePostFailureActionInterface extends Action<typeof CREATE_POST_FAILURE> {
  type: typeof CREATE_POST_FAILURE;
  payload: PostsFailureResponseInterface
}

//Update
export interface UpdatePostRequestActionInterface extends Action<typeof UPDATE_POST_REQUEST> {
  type: typeof UPDATE_POST_REQUEST;
  payload: UpdatePostRequestInterface
}

export interface UpdatePostSuccessActionInterface extends Action<typeof UPDATE_POST_SUCCESS> {
  type: typeof UPDATE_POST_SUCCESS;
  payload: UpdatePostSuccessResponseInterface
}

export interface UpdatePostFailureActionInterface extends Action<typeof UPDATE_POST_FAILURE> {
  type: typeof UPDATE_POST_FAILURE;
  payload: PostsFailureResponseInterface
}

export type PostsActions =
  CreatePostRequestActionInterface
  | CreatePostFailureActionInterface
  | CreatePostSuccessActionInterface
  | UpdatePostRequestActionInterface
  | UpdatePostFailureActionInterface
  | UpdatePostSuccessActionInterface
  | DeletePostRequestActionInterface
  | DeletePostFailureActionInterface
  | DeletePostSuccessActionInterface
  | FetchPostsRequestActionInterface
  | FetchPostsSuccessActionInterface
  | FetchPostsFailureActionInterface
  ;

6) Последний файлик под интерфейс для типизации состояния, которое мы используем в редюсере

interfaces/PostsState.ts
export interface PostsState {
  posts: PostType[];
  successMessage: string | null,
  isLoading: boolean;
  error: string | null;
}

8. Вкратце о компонентах

Так как статья больше про работу с хранилищем - я не вижу особого смысла расписывать о логике компонентов.

Поэтому вкратце.

По небольшой иерархии из компонентов у меня есть

  1. PostsPage.tsx - cтраница ( в ней также лежит попап и состояния для изменения его видимости, а также некоторая логика с библиотекой react-toastify для отображения сообщений по результатам круда ).

  2. PostsList.tsx - компонент списка, который при рендере (на хуке useEffect) диспатчит в состояние результат fetch запроса постов и в цикле вызывает карточку поста.

  3. PostCard.tsx - карточка поста, тут все очевидно.

Также в компонентах имеется AddPostButton.tsx - кнопка для вызова попапа на режим создания нового поста.

PostPopup.tsx - тоже по названию можно понять, что это и есть сам попап.

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

В остальном напомню, что весь код вы можете увидеть здесь.

Заключение

Постарался написать эту статью с наглядными примерами и надеюсь , что кому то это поможет сэкономить время, и плюсом даст понять какая практика является хорошей для того же рефакторинга беспорядочного кода ( шутка про то что redux saga итак беспорядочны тут неактуальна :) ) Спасибо за внимание ! следующая статья на тему фронта будет уже про современный бест практис с RTK query

Теги:
Хабы:
Всего голосов 3: ↑2 и ↓1+1
Комментарии6

Публикации

Истории

Работа

Ближайшие события

Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург