NSwag и react-query — автоматическая генерация hooks для вашего API.

Про использование NSwag и автогенерацию API-клиентов я уже писал несколько раз, у нас в МЦЦ это давно внедрено и используется (чаще всего мы генерируем axios-клиентов).

Однако в последнее время я всё чаще использую react-query — это очень удобная библиотека для кэширования и управления http-запросами. Она не заменяет axios/fetch и им подобные, а работает вместе с ними. Типичный сценарий использования react-query выглядит примерно так:

// объявление функции API-вызова. У нас обычно такие функции уже автогенерируются с помощью nswag
const getPostById = async (key, id) => {
  const { data } = await axios.get(
    `https://jsonplaceholder.typicode.com/posts/${id}`
  );
  return data;
};

// оборачивание этой функции в hook с использованием react-query
function useGetPostById(postId) {
  return useQuery(["post", postId], getPostById, {
    enabled: postId,
  });
}

const Post: React.FC<{ postId: number }> = (props) => {
  // вызов хука внури компонента
  const { status, data, error, isFetching } = useGetPostById(props.postId);
 // ...
}

Я не хочу описывать все плюсы работы с библиотекой (в документации написано намного лучше), но просто не могу не упомянуть про очень удобное кэширование, «магическое» обновление кэша, дедубликацию запросов (чтобы ушел только один запрос, когда вам в двух разных местах нужны одинаковые данные) и даже про работу с Suspense.

Вот про все эти плюсы говорить не буду, а расскажу об одном минусе. Код подобный вышеописанному приходится писать руками для каждого GET-запроса, и если API-вызовы у нас уже сгенерированы, то хук (а чаще еще и функцию-ключ к запросу ["post", postId]) приходится писать руками.

Довольно быстро меня это утомило, и в тот же момент пришла мысль — если мы автогенерируем axios-клиентов, то почему бы не автосгенерировать и это тоже?

Сказано — сделано, nswag основан на гибкой системе liquid-шаблонов, которые с легкостью можно переопределить. Так и родился на свет набор шаблонов nswag-react-query.

Использовать его очень просто. Добавляем в react-проект:

yarn add nswag-react-query nswag react-query

и вызываем автогенерацию (предварительно изменив URL swagger-описания и путь к результирующему файлу)

yarn nswag-react-query /input:https://petstore.swagger.io/v2/swagger.json /output:src/api/axios-client.ts /template:Axios /serviceHost:. /generateConstructorInterface:true /markOptionalProperties:true /generateOptionalParameters:true /nullValue:undefined

После этого хуки, подобные вышеописанным, оказываются авотматически сгенерированными и готовыми к использованию!

Смотрите пример, внедряйте у себя, и не тратьте время на написание рутинного кода!

usePreserveForm — React-хук для сохранения состояния формы при F5 (на основе react-hook-form)

Мы в МЦЦ Томск уже давно и с удовольствием используем react-hooks. В новых компонентах и проектах мы используем исключительно функциональные компоненты, и от оставшихся класс-компонентов тоже потихоньку отказываемся. При переходе на функциональные компоненты мы с удовольствием сменили и компонент для управления формами и валидацией. Мы остановились на react-hook-form.

Есть множество сравнений с Formik или Redux-Form, в которых react-hook-form безоговорочно выигрывает, прежде всего за счет использования неконтролируемых input’ов. Эта техническая деталь делает UX работы с формами в разы лучше: все ведь сталкивались с «тормозами» React’а при заполнении форм (например, когда CPU компьютера сильно нагружен чем-то другим)? Ну так вот с «неконтролируемыми» input’ами это просто невозможно :) Ну и кроме этого важного преимущества, react-hook-form просто приятен для разработчика — для работы с ним требуется минимум дополнительного кода.

Однако, одно из свойств redux-form при переходе на react-hook-form мы потеряли. Например, когда пользователь частично заполнил форму и нажал F5 — все заполненные поля обнулятся. Это особенно обидно, если форма большая и полей заполнено много. Такая потеря данных случается и не только при нажатии F5, а, например, при случайном закрытии браузера, или перезагрузки компьютера. Очень хотелось бы, чтобы пользовательский ввод в таких случаях сохранялся.

В redux-form такое поведение мы получаем «из коробки», здесь же пришлось добавить немного кода. Встречайте, мой первый npm-пакет use-preserved-form. Использовать пакет очень легко — просто вместо вызова useForm необходимо вызывать usePreservedForm.

Подробные инструкции можно также почитать в github-репозитории или посмотреть на пример вживую.

Приятного использования!

How to add ESLint/Prettier support to create-react-app

This is a short reminder for myself about adding ESLint support to a new create-react-app project with typescript (which is my default frontend template for now).

Instructions include setting up file watchers in VSCode and WebStorm, so all files are automatically formatted on save.

So, here we go.

yarn add @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-config-prettier eslint-plugin-prettier

Extract the configs from archive into the folder of a project. This will set up watchers and default configs for both ESLint and Prettier.

Don’t forget to restart your IDE to get all the goodies.

NSwag vs Swashbuckle for Swagger, Typescript client API generation, and fighting undefined/nulls in DTO

I have already expressed my love with Swagger :) Over time, however, I met Swagger’s sister — NSwag — and fell in love with her even more :)

Long story short, NSwag doesn’t have an IFormFile issues I was solving in Swagger out of the box. But the reason I moved is actually a bit different. We wanted to use OpenAPI definitions for autogenerating clients for our API. Writing something like this:

export interface IFirmwareInfoDto {
    exists: boolean;
    versionString: string;
    releaseDate: string;
    deviceGeneration: DeviceGeneration;
}

async getAvailableFiles(options?: AxiosRequestConfig): Promise<IFirmwareInfoDto[]> {
    const response = await axios.get<IFirmwareInfoDto[]>(
        `/api/firmwareDownload/all`,
        {...defaultOpts, ...options},
    );
    return response.data;
},

by hand for every API action is not only tedious, but also error prone. Thank goodness, there are tools for automatic generation of those based on OpenAPI definitions. We used OpenAPI Generator initially (since Swashbuckle doesn’t have anything built-in), and it was good, but, as usual, devil was in the details. And the devil here was C# enum handling.


Actually, OpenAPI Generator had no issues with handling enum. It just doesn’t handle them at all :) There were just two options, both of which were affecting API itself: we could either express enum elements as string or as number. That was suboptimal. So, having a C# DTO and Action like this:

public class UserDto
{
	public string Name { get; set; }
	public DateTime BirthDate { get; set; }
	public Sex Sex { get; set; }
}

[HttpGet]
public UserDto Get(int id)
{
	return new UserDto()
	{
		Name = "Artur",
		BirthDate = new DateTime(1985, 5, 12),
		Sex = Sex.Male,
	};
}

We got the following Dto generated in Typescript

export interface UserDto {
    name?: string | null;
    birthDate?: Date;
    sex?: Sex;
}

export enum Sex {
    NUMBER_0 = 0,
    NUMBER_1 = 1
} 

We could get Sex enum generated like this:

export enum Sex {
    Male = 'Male',
    Female = 'Female'
} 

But only if we configure our API to behave the same way (i.e. you would receive the following JSON as a result of an http call

{
  'name': 'Artur',
  'birthDate': '2009-02-15T00:00:00Z',
  'sex': 'Male'
}

It’s a debatable topic, whether to use strings or ints for enums in the API, but in some cases (like Flags enums, where several values could be combined) ints are unavoidable.

So, we tried NSwag and thankfully, here’s what we have with it:

export enum Sex {
    Male = 0,
    Female = 1,
}

The key difference, is that NSwag has it’s own client generator, and it takes into account some of extensions that NSwag adds when generating API definition (i.e. string representation of enum values are stored in description fields).

Of course, NSwag also generates nice little (well, not so little :)) typescript client as well:

export class UserClient {
    constructor(baseUrl?: string, instance?: AxiosInstance) {
      // ...
    }

    get(id: number): Promise<UserDto> {
      // ...
    }
}

…and we have lived happily ever after with NSwag, unless we discovered another glitch. It’s called undefined. If we take a closer look at UserDto we could notice, that all properties are undefinable (this little ? sign next to the property name):

export interface IUserDto {
    name?: string | undefined;
    birthDate?: Date;
    sex?: Sex;
}

It’s not only inconvenient (because in typescript you would always have to check, if the value is really defined or not), but it’s also just wrong, because with aforementioned c# definition both birthDate and sex will always be defined.

Luckily, this could be fixed rather easy. We have introduced a special RequireValueTypesSchemaProcessor, that you could integrate into NSwag with oneliner:

options.SchemaProcessors.Add(new RequireValueTypesSchemaProcessor());

and DTOs will be automatically fixed for you:

export interface IUserDto {
    name: string;
    birthDate: Date;
    sex: Sex;
}

Go take a look at the sources if you want more details! You will also receive a bonus — the way to easily get back to generating undefinable for some DTOs where you particularly want that (e.g. for HTTP PATCH requests) :)

As usual, you could get complete C# project along with all generated client examples from github. Check out clients folder for already generated clients, or run yarn nswag-client or yarn swashbuckle-nswag-client to regenerate them.