Skip to content

Commit

Permalink
Merge pull request #4 from Selleo/react/refactor
Browse files Browse the repository at this point in the history
feat: add api sections to react docs
  • Loading branch information
k1eu authored Jun 18, 2024
2 parents d2f7adf + 34bc213 commit e3ec203
Show file tree
Hide file tree
Showing 17 changed files with 217 additions and 13 deletions.
2 changes: 1 addition & 1 deletion docs/docs/guide/01-architecture.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Architecture

TODO: explain apporach to typical architecture of the project, discuss environments, CI/CD, monitoring
In progress
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Remix Intro + Setup
# React Intro + Setup

Vite community is huge and Remix intergration with it was the milestone we were waiting for. Now merging the Remix into React Router 7 let's us migrate older projects easier, and have great `framework` environment with patterns for the new apps.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Flexibility of React is a great strength and great pitfall. In this chapter we w

Colocation is gonna be the the theme in the described approach.

- Your code should be inside `app` folder in the Remix app.
- Your code should be inside `app` folder in the React app.
- `modules` is the most important folder inside where most of your UI and business logic is gonna be living. For example we can have `Auth` module that should have everything related to auth inside of it - from compoents to pages, layout, hooks, it's "util functions".
- `api` folder where we'll be storing all data related to the API communication.
- `components` folder should consist of highly reusable components used throughout the whole app - Button, Input
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Data loading

For the data loading we will combine two technologies. `clientloader`s from Remix and React Query `queries`. It will allow us for easier management of queries and less `undefined` states.
For the data loading we will combine two technologies. `clientloader`s from Remix(React Router 7) and React Query `queries`. It will allow us for easier management of queries and less `undefined` states.

For the setup of React Query check their docs: [Installation](https://tanstack.com/query/latest/docs/framework/react/installation) and [Quickstart](https://tanstack.com/query/latest/docs/framework/react/quick-start). With the configuration of the query client you can go one of few ways:

Expand Down Expand Up @@ -80,7 +80,7 @@ export function usePokemonSuspense(id: string) {

## Using queries data

Remix has a loaders concept that we'll utilze here. Loaders as a concept allow you to fetch data before the route component loads which solves eg. the `undefined` data issues. We'll combine it with RQ to easily work with data across components. As we're working in SPA environemnt we'll utilze `clientLoader` export in a route component.
Remix / ReactRouter(6+) has a loaders concept that we'll utilze here. Loaders as a concept allow you to fetch data before the route component loads which solves eg. the `undefined` data issues. We'll combine it with RQ to easily work with data across components. As we're working in SPA environemnt we'll utilze `clientLoader` export in a route component.

```ts
export async function clientLoader() {
Expand Down
35 changes: 35 additions & 0 deletions docs/docs/react-recipes/05-data-manipualtion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Data mutations and refetches

## Data revalidation

With the queryOptions approach for queries data refetches are pretty simple. You simply import necesary queryOptions and refetch on them like:

```tsx
mutate(
{
data: parsedValues,
},
{
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: currentUserQueryOptions.queryKey,
});
},
}
);
```

or

```tsx
mutate(
{
data: parsedValues,
},
{
onSuccess: () => {
queryClient.invalidateQueries(currentUserQueryOptions);
},
}
);
```
60 changes: 60 additions & 0 deletions docs/docs/react-recipes/06-api-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Api client

The common issue in Client <-> Server applications is the issue of typing the data during communcation. We can solve this by generating a OpenAPI schema on our backend and create an API Client on the frontend.

## OpenAPI schema generation

For this purpose on the backend we can use [@nestjs/Swagger](https://docs.nestjs.com/recipes/swagger)

## Frontend consumption

To generate a client on the frontend we have add a dev dependency called
[swagger-typescript-api](https://www.npmjs.com/package/swagger-typescript-api).

```bash
pnpm add -D swagger-typescript-api
```

after that we can create a script that will generate us a new client from a schema file. To do this add this script to you `package.json`

You can configure the path to your BE api-schema.json and file/class names of the generated client.

```json
{
"scripts": {
...,
"generate:client": "swagger-typescript-api -p ../api/src/swagger/api-schema.json -o ./src/api --axios --name base-api.ts --api-class-name BaseApi",
},
}
```

Given command will create a `base.api.ts` file where our BaseApi client will be created. With it you can do anything you want that is availble in the axios client

eg. use interceptors

```ts
export const API = new BaseApi({
baseURL,
headers: {
Authorization: getAuthorizationHeader(),
},
});

API.instance.interceptors.response.use(
(response) => {
return response;
},
(error: AxiosError) => {
if (error.response?.status === 401) {
useAuthStore.getState().setTokens(null, null);
removeAuthHeader();
router.navigate("/login");
}
return Promise.reject(error);
}
);
```

Now all the methods on the API are fully typed and you can use them in the following way

![Api structure](client.png)
File renamed without changes
Binary file added docs/docs/react-recipes/client.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/docs/recipes/04.proxy-and-certs.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ For the tool we will use [Caddy](https://caddyserver.com/)

## How to setup Caddy

// TODO: Docker setup?
Caddy can be installed via [Homebrew on MacOs](https://caddyserver.com/docs/install#homebrew-mac),
Or on other [platforms](https://caddyserver.com/docs/install)

![Monorepo structure](proxy.png)

Expand Down
1 change: 0 additions & 1 deletion docs/docs/remix-recipes/05-data-manipualtion.md

This file was deleted.

1 change: 0 additions & 1 deletion docs/docs/remix-recipes/06-forms.md

This file was deleted.

1 change: 0 additions & 1 deletion docs/docs/remix-recipes/07-api-client.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
QueryClient,
useMutation,
useQueryClient,
} from "@tanstack/react-query";
import { sleep } from "~/utils/sleep";
import { pokemonsOptions } from "../queries/usePokemons";
import { pokemonOptions } from "../queries/usePokemon";

export async function updatePokemon(
_id: string,
_options: { data: { name: string; weight: number } }
) {
await sleep(1000);
return { id: 1, name: "bulbasaur", weight: 69, abilities: [] };
}

export async function invalidatePokemonQueries(
queryClient: QueryClient,
id?: string
) {
await queryClient.invalidateQueries(pokemonsOptions);

if (id) {
await queryClient.invalidateQueries(pokemonOptions(id));
}
}

export function useUpdatePokemon() {
const queryClient = useQueryClient();

return useMutation({
mutationKey: ["updatePokemon"],
mutationFn: ({
id,
options,
}: {
id: string;
options: { data: { name: string; weight: number } };
}) => updatePokemon(id, options),
onSettled: (_data, _error, variables) => {
invalidatePokemonQueries(queryClient, variables.id);
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type PokemonsResponse = {
};

export const pokemonsOptions = queryOptions({
queryKey: ["pokemons"],
queryKey: ["pokemons", "list"],
queryFn: async () => {
const response = await fetch("https://pokeapi.co/api/v2/pokemon");
return response.json() as Promise<PokemonsResponse>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import {
ClientActionFunctionArgs,
ClientLoaderFunctionArgs,
Form,
isRouteErrorResponse,
useFormAction,
useParams,
useRouteError,
} from "@remix-run/react";
import { pokemonOptions, usePokemonSuspense } from "~/api/queries/usePokemon";
import { useState } from "react";
import {
invalidatePokemonQueries,
updatePokemon,
useUpdatePokemon,
} from "~/api/mutations/useUpdatePokemon";
import {
pokemonOptions,
usePokemon,
usePokemonSuspense,
} from "~/api/queries/usePokemon";
import { queryClient } from "~/api/queryClient";

export async function clientLoader({ params }: ClientLoaderFunctionArgs) {
Expand All @@ -23,19 +36,70 @@ export async function clientLoader({ params }: ClientLoaderFunctionArgs) {
return {};
}

export async function clientAction({
request,
params,
}: ClientActionFunctionArgs) {
if (!params.id) throw new Error("No id provided");
const formData = await request.formData();

const name = formData.get("name") as string;
const weight = Number(formData.get("weight"));

await updatePokemon(params.id, { data: { name, weight } });
await invalidatePokemonQueries(queryClient, params.id);

return {};
}

export default function PokemonPage() {
const params = useParams<{ id: string }>();
const { data: pokemon } = usePokemonSuspense(params.id!);
const { data: pokemon, isFetching } = usePokemonSuspense(params.id!);
const [editMode, setEditMode] = useState(false);

return (
<main>
<h1>{pokemon.name} page</h1>
<header>
<h1>{pokemon.name} page</h1>
<h2>{isFetching && "Refetching in bg..."}</h2>
<button
onClick={() => {
setEditMode(!editMode);
}}
>
edit
</button>
</header>
<p>Here you can see the pokemon details</p>
<img
alt={pokemon.name}
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.id}.png`}
/>
<p>Weight: {pokemon.weight}</p>

{editMode && (
<Form method="POST">
<label>
Name:
<input
className="text-black"
type="text"
name="name"
defaultValue={pokemon.name}
/>
</label>
<label>
Weight:
<input
className="text-black"
type="number"
name="weight"
defaultValue={pokemon.weight}
/>
</label>
<button type="submit">Save</button>
</Form>
)}
</main>
);
}
Expand Down
3 changes: 3 additions & 0 deletions examples/common_nestjs_remix/apps/web/app/utils/sleep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

0 comments on commit e3ec203

Please sign in to comment.