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>
);
};