# Page, Containers & Form state management
> TARGET AUDIENCE: This document is a required reading for developers.
In this document we will cover all aspects of a Page anatomy and how it interacts with Containers. After reading this document you will be able to answer the following questions:
- What is a Page?
- What are the responsibilities of a Page?
- What is a Container?
- What are the responsibilities of a Container?
- What is a Form?
- How Form state management works?
- How to manage dependencies?
# PAGE
## What is a Page?
A Page is a sealed component. It means a Page doesn't receive `props`. The typical use case of a page is in the main `<App .. />` mounted over an app `<Route .. />`.
Lets write some code:
```javascript
import { Routes, Route } from "react-router-dom";
import { HomePage, ProductsPage, CheckoutPage } from "./pages";
const homePage = <HomePage />;
const productsPage = <ProductsPage />;
const checkoutPage = <CheckoutPage />;
export const App = () => (
<Routes>
<Route path="/">
<Route element={homePage} path="/" />
<Route element={productsPage} path="/products/:category" />
<Route element={checkoutPage} path="/checkout" />
</Route>
</Routes>
);
```
At this point a Page is whatever component given that it does not need nor receive `props`.
Additionally, a Page is a navigable component, it means that if we switch pages (i.e. we navigate from a Page to another Page) the browser history will be able to go back and forth between them.
## What are the responsibilities of a Page?
To understand a Page we need to understand a Container.
The Container represents a single use case. For instance, a Container could be a form to checkout a payment. A Page does not implement the use case, the use case is implemented in the Container and the Page configures the Container with all it dependencies (i.e. using props).
A Page in this _components system_ have well-defined responsibilities:
- Decorate the Container with the PageTemplate.
- Provides translations to the PageTemplate.
- The Page pass down into the Container the API methods to retrieve and mutate data from/to online services. The container will be responsible of fetching or updating the data WHEN required using the provided methods.
- Maps values from the API in and out of the Container. When the Page pass down the API methods to the container it attaches the in/out mappers (using a closure) to the APIs methods.
- Pass down to the Container any other dependency that lives at Page level (e.g. a container argument)
Long story short, we can see the _single responsibility_ of the Page is to provide dependencies to the container. Also, it conveniently decorates the container with the `<PageTemplate />` if required.
Let's continue with the example we wrote on [REST APIs](/hK0qJr7OQa-_ERCDRd-7xA?view#Using-the-API-request-at-Page-level1):
```typescript=
import { useTranslationContext } from '@cronosccs/react-components
import { Translation } from '../translations';
import { useParams } from 'react-router';
import { PageTemplate } from '../../components';
import { getProducts, postFavorite} from '../../services';
import { ProductsContainer } from './productContainer';
import { mapProducts, mapFavorite} from './mappers';
export type ProductsTranslations = Pick<
Translation,
'UI_PRODUCTS_TILE' | 'UI_PRODUCTS_SUBTITLE'
>;
export const pageTemplateProps = (texts: ProductsTranslations) => ({
title: texts.UI_PRODUCTS_TILE,
subtitle: texts.UI_PRODUCTS_SUBTITLE,
});
export const ProductsPage = () => {
const texts = useTranslationContext<ProductsTranslations>();
const axiosInstance = useAxiosInstance();
const { category } = useParams();
return (
<PageTemplate {...pageTemplateProps(texts)}>
<ProductsContainer
getProducts={getProducts(axiosInstance, mapProducts)}
postFavorite={postFavorite(axiosInstance, mapFavorite)}
category={category}
/>
</PageTemplate>
);
};
```
Now, let's check Page constraints:
- [**DONE**] Decorate the Container with the PageTemplate.
- [**DONE**] Provides translations to the PageTemplate.
- [**DONE**] Pass API methods down to the Container.
- [**DONE**] Maps values from the API in/out the Container.
- [**DONE**] Pass down to the Container any other dependency.
So, we just added the `PageTemplate` to the Page, mapped the [Translations](/rOfTxqrcSUiDKa6FrQXilQ) context, and extracted the methods `mapProducts` and `mapFavorite` to an external file named `mappers.ts`.
Additionally, we passed to the container the router param `category`. As the router param is an implementation detail of the Page itself, we provide it as prop to the container.
# CONTAINER
## What is a Container?
As mentioned, a Container represents a single use case. It means that the container manages the _business rules_ of this part of the application, it means the state management of the use case.
Note that in this example we will use simple state management based on `useState` to hold our products. Later on we will work on a Container with a more complex scenario (a `<form>`) and will explain how we manage it.
## What are the responsibilities of a Container?
A Container in this _components system_ have well-defined responsibilities:
- Manage the state management of the use case.
Let's continue with the example we used in the [REST APIs](/hK0qJr7OQa-_ERCDRd-7xA?both#Mutation-data-at-Container-level) document.
The big difference here is that we refactored the code to extract the `useQuery`, `useMutation` dependencies to a high level custom hooks in the file `hooks.ts`. We also extracted the JSX code to a view to the file `productsForm.tsx`.
This refactor will help us to keep organized code and help to provide _single responsibility_ to the container: Manage the use case.
File: `productsContainer.tsx`
```typescript=
import { useState } from "react";
import type { Product } from "./types"
import { useProducts, useMarkAsFavorite } from "./hooks";
type ProductsContainerProps = {
getProducts: () => Promise<Product[]>;
postFavorite: (variables: { product: string; }) => Promise<void>;
};
export const ProductsContainer = ({ getProducts, postFavorite }: ProductsContainerProps) => {
const [products, setProducts] = useState<Product[]>([]);
const { isLoading, error } = useProducts(getProducts, setProducts);
const { isMutating, markAsFavorite } = useMarkAsFavorite(postFavorite);
return (
<ProductsView
products={products}
isLoading={isLoading}
markAsFavorite={markAsFavorite}
/>
);
};
```
We extracted the type `Product` to a `types.ts` file in order to prevent circular dependencies.
File: `types.ts`
```typescript=
export type Product = {
name: string;
price: number;
categories: string[];
};
```
Here we extracted the `useProducts` and `useMarkAsFavorite` custom hooks.
File: `hooks.ts`
```typescript=
import { Dispatch, SetStateAction } from "react";
import { useQuery, useMutation } from "react-query";
export const useProducts = (getProducts: () => Promise<Product[]>, setProducts: Dispatch<SetStateAction<Product[]>>) => {
const productsQuery = useQuery({
queryKey: "getProducts",
queryFn: getProducts,
onSuccess: (p) => {
setProducts(p);
},
});
const isLoading = productsQuery.isLoading;
const error = productsQuery.error;
return { isLoading, error };
}
export const useMarkAsFavorite = (postFavorite: (variables: { product: string; }) => Promise<void>) => {
const favoriteMutation = useMutation('postFavorite', postFavorite);
const markAsFavorite = (product: string) => {
favoriteMutation.mutate({ product });
}
const isMutating = favoriteMutation.status == 'loading';
return { isMutating, markAsFavorite };
}
```
Finally, we extracted the _view_ to an external file called `productsView.tsx`.
```typescript=
import type { Product } from "./types"
type ProductsViewProps = {
isLoading: boolean;
products: Product[];
markAsFavorite = (product: string) => void;
};
export const ProductsView = ({ isLoading, products, markAsFavorite}: ProductsViewProps) => {
return (
<>
{!!isLoading && <p>Loading...</p>}
<ul>
{products.map((product) => (
<li key={product.name}>
Product: {product.name} Price: {product.price.toFixed(2)}
<button onClick={() => markAsFavorite(product.name)}>Mark As Favorite</button>
</li>
))}
</ul>
</>
);
};
```
# FORM STATE MANAGEMENT
## Basic Form
There are more complex use cases, such as forms, where we will need something extra to manage the use case state. For example, in a form, we need to perform validations and run other rules before performing an action in the domain services.
To manage this scenarios we will use [react-hook-forms](https://react-hook-form.com/). To simplify the usage we created a few wrappers to "hide" the implementation details of _react-hook-forms_ as much as possible at the container level. This is done by using `useFormContext` within components. The objective of this simplification is to avoid props-drilling between multiple nested components which is error-prone and makes the container logic harder to reasoning.
If the use case requires a form, the container should work as the example below:
In this scenario, we are assuming the following dependencies are already there (we already covered how to write these dependencies quite well).
- parent `Page` is already in place and configures the container accordingly.
- custom hook named `useUpdateProduct`: mutate a single product update by name.
pages/updateProduct/types.ts
```typescript=
export type Product = Partial<{
name: string;
price: number;
categories: string[];
}>;
```
pages/updateProduct/hook.tsx
```typescript=
import { type UseFormReturn } from 'react-hook-form';
import { useQuery, useMutation } from 'react-query';
export const useGetProduct = (
getProduct: () => Promise<Product>,
methods: UseFormReturn<Product, void>,
) => {
const productQuery = useQuery({
queryKey: 'getProduct',
queryFn: getProduct,
onSuccess: (data) => {
methods.setValue('name', data.name);
methods.setValue('price', data.price);
methods.setValue('categories', data.categories);
},
refetchOnWindowFocus: false,
});
const isLoadingProduct = productQuery.isLoading;
const productError = productQuery.error;
return { isLoadingProduct, productError };
};
/* useUpdateProduct goes here */
```
pages/updateProduct/updateProductContainer.tsx
```typescript=
import type { Product } from './types';
import { useGetProduct, useUpdateProduct } from './hooks';
type UpdateProductContainerProps = {
getProduct: (name: string) => Promise<Product>;
postProduct: ({ variables: Product }) => Promise<void>;
};
const rules = {
name: { required: true },
price: { required: true },
categories: {}
};
export const UpdateProductContainer = ({ getProduct, postProduct }: UpdateProductContainerProps) => {
const methods = useForm<Product>();
const { isLoadingProduct, productError } = useGetProduct(getProduct, methods);
const { updateProduct, mutationStatus, mutationError } = useUpdateProduct(postProduct);
return (
<Form methods={methods} rules={rules} onSubmit={updateProduct}>
{(productError != null) && <p>Error Loading : {productError}</p>}
{!!isLoading && <p>Loading...</p>}
{(!!isLoading && productError == null) && (
<TextInput name="name" />
<PriceInput name="price" />
<DropdownMultiple name="categories" options={defaultCategories} />
<Button type="submit" text="SAVE" />
)}
</Form>
);
};
```
In the example above, we added 3 different input elements connected to the form (by setting the name). The component will register itself to the Form and configure its own validation. Check the list of available components ant its documentation.
Also, pay attention to the `useGetProduct` hook. In this hook we are setting the initial values of the form elements.
> NOTE: we are working on a new version of this approach that moves the `setValue` logic to the Container. The rationale is that we don't want to hide "how" the values are set in the container context. Current approach delegates Container responsibility to the hook which is NOT what we are looking for.
## Dependencies
In this scenario we have a Form with two Dropdowns and the content on the second Dropdown depends on the fist one.
To handle this dependency, we will combine a nice feature of react-hook-forms, the custom hook `useWatch` and a nice feature of the `useQuey` custom hook, the `enabled` property.
In the `hooks.ts` file, we will have a `useData` custom hook to wrap the data query as the example below.
```typescript=
export const useData = (
getData: () => Promise<Data>,
methods: UseFormReturn<FormData, void>,
) => {
const dataQuery = useQuery({
queryKey: 'getData',
queryFn: getData,
onSuccess: (data) => {
methods.setValue('data', data);
},
refetchOnWindowFocus: false,
});
const isLoadingData = dataQuery.isLoading;
const dataError = dataQuery.error;
return { isLoadingData, dataError };
};
```
Then, the dependent query (the one that must be loaded once the user selects an option of the first Dropdown), will be as the example below:
```typescript=
export const useDependentData = (
selectedData: Data | undefined,
getDependentData: () => Promise<DependentData>,
methods: UseFormReturn<FormData, void>
) => {
const dependentDataQuery = useQuery({
queryKey: 'getDependentData',
queryFn: async () => await getDependentData(selectedData!),
onSuccess: (data) => {
methods.setValue('dependentData', data);
},
enabled: selectedData != null,
refetchOnWindowFocus: false,
});
const isLoadingData = dependentDataQuery.isLoading;
const dataError = dependentDataQuery.error;
return { isLoadingDependentData, dependentDataError };
};
```
In the example above, we are receiving a new argument named `selectedData` which Type is either `Data` or `undefined`. This is because, at the time of the container load, the value of `Data` will be `undefined` until the user selects an option in the data Dropdown.
Now, pay attention to lines 8 and 12.
In line 8, we are invoking the `getDependentData` passing the `selectedData` as an argument (of course, the `getDependentData` does expect that argument) using the ["Not-Null assertion operator"](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#non-null-assertion-operator-postfix-) to tell typescript we are SURE that data isn't undefined at this point.
This is being validated at the line 12 (unfortunately, typescript doesn't have a way to know that) when we check that `selectedData != null`. As `useQuery` will not invoke the `queryFn` until enabled is true, we know that both conditions are true. The data option was already selected and the value of `selectedData` isn't undefined.
Finally, to connect both custom hooks we will to use the `useWatch` react-hook-forms custom hooks. Let's see an example:
```typescript=
/// ... more code here
export const DataAndDependantDataContainer = (...) => {
const methods = useForm<FormData>();
/// ... more code here
const { isLoadingData, dataError } = useData(getData, methods)l
const selectedData = useWatch({ name: 'data', control: methods.control });
const { isLoadingDependentData, dependentDataError } = useDependentData(selectedData, getDependentData, methods);
/// ... more code here
}
```
Now, once the user selects an option in the data Dropdown, the `useDependentData` will trigger, obtain the data from the `getDependantData` promise and do whathever it have to do.