The Application
In the previous lab we built a simple shopping application using fp-ts
. In
this lab we will build off the previous application to create a more pure
network connection. From the learning of
pure network request with TaskEither,
I'll refactor the application and presentation layer to include the
useTaskEither
hook and separate the fetchProduct
from the useShoppingCart
hook.
Application Layer
The refactored application layer adds the useTaskEither
hook. This hook is
responsible for running the TaskEither
and updating the state of the
application. It also adds the match
function. This function is responsible for
pattern matching over all possible states of the application. This allows us to
separate the states of the application in the presentation layer.
function useTaskEither<E, A>(timeToLoad: number) {
const [value, setValue] = useState<O.Option<E.Either<E, A>>>(O.none);
const [isLoading, setIsLoading] = useState<boolean>(false);
const run = useCallback((te: TE.TaskEither<E, A>) => {
const start = performance.now();
const task = pipe(
TE.fromIO(() => setIsLoading(true)),
TE.chain(() => te),
TE.chainFirstTaskK(() => delay(timeToLoad - (performance.now() - start))),
TE.chainFirst(() => TE.fromIO(() => setIsLoading(false))),
);
task().then((either) => pipe(either, O.some, setValue));
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const match = useCallback(
<B, C, D, F>(
onNone: () => B,
onLoading: () => C,
onError: (error: E, isLoading: boolean) => D,
onSuccess: (value: A, isLoading: boolean) => F,
) =>
pipe(
value,
O.matchW(
() => (isLoading ? onLoading() : onNone()),
E.matchW(
(e) => onError(e, isLoading),
(a) => onSuccess(a, isLoading),
),
),
),
[value, isLoading],
);
return [match, run] as const;
}
I've also separated the fetchProduct
function from the useShoppingCart
hook.
const useShoppingCart = () => {
const [error, setError] = useState<O.Option<AppError>>(O.none);
const [cart, setCart] = useState<Cart>({ items: [] });
const addItem = (product: Product, amount: number | undefined = 1) =>
pipe(
cart,
updateCart(product, amount),
E.fold(
(error) => setError(O.some(error)),
(cart) => setCart(cart),
),
);
const removeItem = (product: Product, amount: number | undefined = 1) =>
pipe(
cart,
updateCart(product, -amount),
E.fold(
(error) => setError(O.some(error)),
(cart) => setCart(cart),
),
);
return { cart, addItem, removeItem, error };
};
Presentation Layer
The refactored presentation layer uses the match
function to pattern match
over all possible states of the application. This allows us to separate the
states of the application in the presentation layer.
export const ShoppingApp: FC = () => {
const [matchProducts, runProducts] = useTaskEither<AppError, Product[]>(1000);
useEffect(() => runProducts(getProductList()), []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<main>
{matchProducts(
() => null,
() => (
<LoadingCard />
),
(error, isLoading) => (
<ErrorCard
type={error.type}
isLoading={isLoading}
onRetry={() => runProducts(getProductList())}
/>
),
(products, isLoading) => (
<ShopCard products={products} isLoading={isLoading} />
),
)}
<Toaster />
</main>
);
};