Managing application cache with react-query. And code generation from OpenAPI.
Tags: react, codegen, reactquery, openapi
Published: 2022-05-31 16:18:06

## Introduction
In this article, I would like to address the following aspects:
- What is application cache.
- react-query as a way to manage application cache.
- how on the project we use code generation from Open API in `npm package` with custom `react-query` hooks and further we spread the code between two clients of Web i Mobile.

Until recently, the Web application on the project I'm working on used `Redux` as the primary state manager, but now we've completely switched to `react-query`. Let's take a look at what I personally think are the disadvantages of `Redux` and why `react-query`?

Why did `Redux` take on the many projects by default? My answer is that thanks to `Redux` we have architecture. That is, we have a Store in which we store the State of the entire application, we have Actions that we Dispatch when we need to change the store. And all the asynchronous operations we do are through crutch `middleware` using mostly Thunk and Saga etc.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pulad7w6dbegnt2a2cvz.png)

Now we realize, that the cool thing is that `Redux` helps to make the architecture - what's wrong with it. I repeat this is my personal experience with him you can disagree.

## Disadvantages of Redux:

**1. Verbosity.**

It's not very cool when you need to develop some kind of module in an existing application, constantly writing a bunch of code. Switching between different modules with. Action_type, action creators, Thunks, etc.
Writing fewer boilerplates not only increases the chance of making fewer mistakes, but also increases the readability of the code - and this is very cool, since you have to read and understand more often than write.

> «All code is buggy. It stands to reason, therefore, that the more code you have to write the buggier your apps will be.» — [RICH HARRIS](https://twitter.com/Rich_Harris)

**2. Everything is stuffing into it.**

When you're working on a big project with multiple developers. Again, this is my experience. The element of rush and deadlines encourages developers to start storing everything in the global store, even if we don't have to. Conditionally synchronous “handles” that switch private ui behavior in single modules. Requests to the server that are also used in the same module. All this is moved to the global store, and can obfuscate the code by increasing its cohesion.

**3. Redux creates non-obvious hidden dependencies.**

An example to get the data we get users in the **Home.js** component:
```typescript
React.useEffect(() => {
      dispatch(getUserData()); 
  }, []);
```
And then having received the data, we use them in many other components (**Transactions**, **Items**, **Menu** ..). In this case, this creates a hidden dependency, because when refactoring the code, if we remove this dispatch(getUserData()) in only one place, it breaks userData in all other places in the application.
And more importantly, the mechanism for maintaining the data that we received from the server is not convenient. We constantly need to monitor the validity of this data and remember to update it if we know that it has changed on the server.

And here we come to 2 concepts of data in an application. We can split the data into **State** and **Cache**.

**States** are the data that needs to be saved and changed throughout the life of the application.
**Cache** is data received from outside, let's say http request.

And in the redux, we mix and store them in a state just because they are used in other places in the application.
So 90% of the data that we use in the application is cache.

At this point, I want to move on to the [react-query](https://react-query.tanstack.com/) cache management library. Give a brief overview and see how you can improve your developer experience with cache using this library.

## Overview of React-Query

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hlhhoy9h46o4czdf8dsc.png)

As written on the official site: **Fetch, cache, and update data in your React and React Native applications all without touching any "global state"**. At their core, these are custom hooks that take control of the cache, giving us a lot of cool features, such as caching, optimistic update, etc. ... And what I like is that it removes a lot of intermediate abstractions, reducing the amount of code written. Let's go with an example.

Everything is simple here, we wrap the root of our application in a `QueryClientProvider`:
```typescript
import { QueryClient, QueryClientProvider } from 'react-query'
  const queryClient = new QueryClient()
  export default function App() {
   return (
     <QueryClientProvider client={queryClient}>
       <ExampleFirst />
     </QueryClientProvider>
   )
 }
```

Now we make a request in the component using **axios** get, which we pass to `useQuery`:
```typescript
import {  useQuery } from 'react-query'
import axios from 'axios'

 function ExampleFirst() {
   const { isLoading, error, data } = useQuery('repoData', async () =>
    const res = await axios.get('https://api.github.com/repos/react-query')
    return res.data
   )
 
   if (isLoading) return 'Loading...'
   if (error) return 'An error has occurred: ' + error.message
 
   return (
     <div>
       <h1>{data.name}</h1>
       <p>{data.description}</p>
       <strong>👀 {data.subscribers_count}</strong>{' '}
       <strong>✨ {data.stargazers_count}</strong>{' '}
       <strong>🍴 {data.forks_count}</strong>
     </div>
   )
 }
```
We wrapped our request in a `useQuery` hook and got an API for working with data, and we leave control over loading, processing and interception of errors to the hook. `useQuery` takes as its first parameter a unique query key. `react-query` manages query caching based on query keys. Query keys can be as simple as a string, or as complex as an array of multiple strings and nested objects. The second parameter is our get request, which returns a promise. And the third, optional, is an object with additional configurations.

As you can see, this is very similar to the code when we learned how to work with server requests in React, but then everything turned out differently on a real project :) And we began to apply a large layer of abstractions on top of our code to catch errors, load status and everything else. In `react-query`, these abstractions are brought under the hood and leave us with purely convenient APIs to work with.

In fact, this is the main example of using `react-query` hooks for get requests. In fact, the API of what the hook returns is much larger, but in most cases we use these few `{ isLoading, error, data }`

`useQuery` also shares state with all other useQuery with the same key. You can call the same useQuery call multiple times in different components and get the same cached result.

For queries with data modification, there is a `useMutation` hook. Example:
```typescript
export default function App() {
  const [todo, setTodo] = useState("");

  const mutation = useMutation(
    async () =>
      axios.post("https://jsonplaceholder.typicode.com/todos", {
          userId: 1,
          title: todo,
        }),
    {
      onSuccess(data) {
        console.log("Succesful", data);
      },
      onError(error) {
        console.log("Failed", error);
      },
      onSettled() {
        console.log("Mutation completed.");
      }
    }
  );

  async function addTodo(e) {
    e.preventDefault();
    mutation.mutateAsync();
  }

  return (
    <div>
      <h1>useMutations() Hook</h1>
      <h2>Create, update or delete data</h2>
      <h3>Add a new todo</h3>
      <form onSubmit={addTodo}>
        <input
          type="text"
          value={todo}
          onChange={(e) => setTodo(e.target.value)}
        />
        <button>Add todo</button>
      </form>
      {mutation.isLoading && <p>Making request...</p>}
      {mutation.isSuccess && <p>Todo added!</p>}
      {mutation.isError && <p>There was an error!</p>}
    </div>
  );
}
```
Again, we pass `axios.post(..)` to the hook, and we can directly work with the `{isLoading, isSuccess, isError}` API and other values ​​that useMutation provides. And we call the mutation itself using `mutation.mutateAsync ()`. In this example, we see that we are passing an object with functions as the second parameter:

- this will work on successful completion of the post request and will return the data we received:
```typescript
onSuccess(data) {
        console.log("Succesful", data);
  }
```
- will works if an error occurred, return an error:
```typescript
onError(error) {
        console.log("Failed", error);
      },
```
- will work anyway, after query triggers:
```typescript
onSettled() {
        console.log("Mutation completed.");
      }
```
In this object, we can put additional keys in order to control the data fetching process.

`useMutation` will keep track of the state of the mutation in the same way that `useQuery` does for queries. This will give you the `isLoading`, `isFalse` and `isSuccess` fields so that you can easily display what is happening to your users. The difference between `useMutation` and `useQuery` is that `useQuery` is declarative, `useMutation` is imperative. By this I mean that `useQuery` queries are mostly done automatically. You define the dependencies, but `useQuery` will take care of executing the query immediately, and then also perform smart background updates if necessary. This works great for requests because we want what we see on the screen to be in sync with the actual data from the back-end. It won't work for mutations. Imagine that every time you focus the browser window, a new task will be created. So instead of triggering a mutation immediately, React Query provides you with a function that you can call whenever you want to mutate.

It is also recommended to create a custom hook in which we put our react-query hook:
```typescript
const transformTodoNames = (data: Todos) =>
  data.map((todo) => todo.name.toUpperCase())
export const useTodosQuery = () =>
  useQuery(['todos'], fetchTodos, {
    select: transformTodoNames,
  })
```
This is convenient because:

- you can store all uses of a single query key (and possibly type definitions) in a single file;
- if you need to tweak some settings or add data transformation, you can do it in one place.

And at this point, when familiarity with react-query is over. I'd like to show you how we can go even further with react-query and generate our hooks from an OpenAPI schema.

## Code generation from OpenAPI

As we can see, all requests are separate hooks without being tied to store abstractions. Therefore, if we have a valid `OpenApi` schema with a back-end, we can code-generate our hooks directly from the schema, and put it in a separate npm package. What will this give us:

- reduce the amount of manual work and boilerplate writing;
- simplify the architecture of the application;
- less code === less bugs
- we will reuse code on the web client, and on the mobile react native client.

> **From Wikipedia:** The OpenAPI specification, originally known as Swagger, is a specification for machine-readable files with interfaces to describe, create, consume, and render REST web services.

I do not want to focus on the `OpenApi` scheme, it is better to read about it on certain resources. But we will assume that we have the actual `OpenAPI` json scheme of our REST requests. Next is an example of our custom library, which we use in our project. I'll go over the main points quickly to convey the general idea. Let's create a new project with the following structure:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p4eb40ip4zkru0o8hj6p.png)

src/operations/index.ts:
```typescript
export * from './operations'; 
```
In `.openapi-web-sdk-generatorrc.yaml` we need to configure the options:
```yaml
generators:
  - path: "@straw-hat/openapi-web-sdk-generator/dist/generators/react-query-fetcher"
    config:
      outputDir: "src/operations"
      packageName: "@super/test"
```
package.json:
```json
{
  "name": "@super/test",
  "version": "1.0",
  "description": "test",
  "license": "UNLICENSED",
  "scripts": {
    "prepack": "yarn build",
    "codegen:sdk": "sht-openapi-web-sdk-generator local --config='./specification/openapi.json'"
  },
  "type": "commonjs",
  "main": "dist/index.js",
  "typings": "dist/index.d.ts",
  "files": [
    "dist",
  ],
  "dependencies": {
    "@straw-hat/react-query-fetcher": "^1.3.1"
  },
  "peerDependencies": {
    "@straw-hat/fetcher": "^4.8.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-query": "^3.34.8"
  },
  "devDependencies": {
    "@straw-hat/fetcher": "^4.8.2",
    "@straw-hat/openapi-web-sdk-generator": "^2.4.2",
    "@straw-hat/tsconfig": "^3.0.2",
    "@types/jest": "^27.4.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-query": "^3.34.12"
  }
}
```
We use one package for code generation, all the others are needed so that our generated hooks receive dependencies after generation:
```typescript
@straw-hat/openapi-web-sdk-generator
```
If we look at what this package is based on, we will see that we are using [oclif](https://oclif.io/) - this is a node.js-based tool for creating a CLI.

[Mustache.js](https://github.com/janl/mustache.js) is a template engine for creating js templates. [cosmiconfig](https://www.npmjs.com/package/cosmiconfig?activeTab=readme#usage) is a tool to make it convenient to work with the configuration.

In package.json we configure:
```json
"oclif": {
    "commands": "./dist/commands",
    "bin": "sht-openapi-web-sdk-generator",
    "plugins": [
      "@oclif/plugin-help"
    ]
  }
```
Let's look in **./dist/commands**, we have the `local.ts` file there:
```typescript
import { flags } from '@oclif/command';
import { OpenapiWebSdkGenerator } from '../openapi-web-sdk-generator';
import { readOpenApiFile } from '../helpers';
import { BaseCommand } from '../base-command';

export default class LocalCommand extends BaseCommand {
  static override description = 'Generate the code from a local OpenAPI V3 file.';

  static override flags = {
    config: flags.string({
      required: true,
      description: 'OpenAPI V3 configuration file.',
    }),
  };

  async run() {
    const { flags } = this.parse(LocalCommand);

    const generator = new OpenapiWebSdkGenerator({
      context: process.cwd(),
      document: await readOpenApiFile(flags.config),
      config: this.configuration,
    }).loadGenerators();

    return Promise.all(generator.generate());
  }
}
```

We will inherit `LocalCommand` from `BaseComand` - this abstract class BaseCommand extends Command is the class that serves as the basis for each oclif command. And in the `run()` function, we set up the config and return `Promise.all(generator.generate())`; generator is an instance of the `OpenapiWebSdkGenerator` class with a description of the generator logic. This will be our code generation command.

Now let's see what are our classes from which we generate code: `src/generators/react-query-fetcher`

Here is how we generate code from a template:
```typescript
import { CodegenBase } from '../../codegen-base';
import { OperationObject, PathItemObject } from '../../types';
import { forEachHttpOperation, getOperationDirectory, getOperationFileRelativePath } from '../../helpers';
import path from 'path';
import { OutputDir } from '../../output-dir';
import { TemplateDir } from '../../template-dir';
import { camelCase, pascalCase } from 'change-case';
import { OpenAPIV3 } from 'openapi-types';

const templateDir = new TemplateDir(
  path.join(__dirname, '..', '..', '..', 'templates', 'generators', 'react-query-fetcher')
);

function isQuery(operationMethod: string) {
  return OpenAPIV3.HttpMethods.GET.toUpperCase() == operationMethod.toUpperCase();
}

export interface ReactQueryFetcherCodegenOptions {
  outputDir: string;
  packageName: string;
}

export default class ReactQueryFetcherCodegen extends CodegenBase<ReactQueryFetcherCodegenOptions> {
  private readonly packageName: string;
  readonly #outputDir: OutputDir;

  constructor(opts: ReactQueryFetcherCodegenOptions) {
    super(opts);
    this.#outputDir = new OutputDir(this.options.outputDir);
    this.packageName = opts.packageName;
  }

  #processOperation = async (args: {
    operationMethod: string;
    operationPath: string;
    pathItem: PathItemObject;
    operation: OperationObject;
  }) => {
    const operationDirPath = getOperationDirectory(args.pathItem, args.operation);
    const operationFilePath = `use-${getOperationFileRelativePath(operationDirPath, args.operation)}`;
    const functionName = camelCase(args.operation.operationId);
    const typePrefix = pascalCase(args.operation.operationId);
    const pascalFunctionName = pascalCase(args.operation.operationId);
    const operationIndexImportPath = path.relative(
      this.#outputDir.resolveDir('index.ts'),
      this.#outputDir.resolve(operationFilePath)
    );

    await this.#outputDir.createDir(operationDirPath);

    const sourceCode = isQuery(args.operationMethod)
      ? await templateDir.render('query-operation.ts.mustache', {
          functionName,
          typePrefix,
          pascalFunctionName,
          importPath: this.packageName,
        })
      : await templateDir.render('mutation-operation.ts.mustache', {
          functionName,
          typePrefix,
          pascalFunctionName,
          importPath: this.packageName,
        });

    await this.#outputDir.writeFile(`${operationFilePath}.ts`, sourceCode);
    await this.#outputDir.formatFile(`${operationFilePath}.ts`);

    await this.#outputDir.appendFile(
      'index.ts',
      await templateDir.render('index-export-statement.ts.mustache', {
        operationImportPath: operationIndexImportPath,
      })
    );
  };

  async generate() {
    await this.#outputDir.resetDir();
    await forEachHttpOperation(this.document, this.#processOperation);
    await this.#outputDir.formatFile('index.ts');
  }
}
```
We see that according to different conditions that we take from the schema, we generate useQuery or useMutation templates from the `query-operation.ts.mustache` or `mutation-operation.ts.mustache` template, respectively:

```typescript
import type { Fetcher } from '@straw-hat/fetcher';
import type { UseFetcherQueryArgs } from '@straw-hat/react-query-fetcher';
import type { {{{typePrefix}}}Response, {{{typePrefix}}}Params } from '{{{importPath}}}';
import { createQueryKey, useFetcherQuery } from '@straw-hat/react-query-fetcher';
import { {{{functionName}}} } from '{{{importPath}}}';

type Use{{{pascalFunctionName}}}Params = Omit<{{{typePrefix}}}Params, 'options'>;

type Use{{{pascalFunctionName}}}Args<TData, TError> = Omit<
  UseFetcherQueryArgs<{{{typePrefix}}}Response, TError, TData, Use{{{pascalFunctionName}}}Params>,
  'queryKey' | 'endpoint'
>;

const QUERY_KEY = ['{{{functionName}}}'];

export function use{{{pascalFunctionName}}}QueryKey(params?: Use{{{pascalFunctionName}}}Params) {
  return createQueryKey(QUERY_KEY, params);
}

export function use{{{pascalFunctionName}}}<TData = {{{typePrefix}}}Response, TError = unknown>(
  client: Fetcher,
  args: Use{{{pascalFunctionName}}}Args<TData, TError>,
) {
  return useFetcherQuery<{{{typePrefix}}}Response, TError, TData, Use{{{pascalFunctionName}}}Params>(client, {
    ...args,
    queryKey: QUERY_KEY,
    endpoint: {{{functionName}}},
  });
}
```
Excellent! Very superficially figured out how our code generation works.

## Finishing and starting the generator

Let's return to the test project. We take the `OpenAPI` schema and put it in the specification folder:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yco7sf362mj2pq33qlvy.png)
What remains for us is to run the command in the console:
 
```bash
yarn codegen:sdk
```
In the console we see something like:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7bb69bkd7kkgga3g9tkw.png)
All of our custom hooks are generated and we can see them in the operations folder:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6csmgl1u6ovjdq6we2xl.png)

Now we can download and use these hooks as a standalone `npm package` in our project.