Lloyd Richards Design
FP-TS
IO-TS

Oct 8, 2023

fp-ts: useTaskEither (Part 4)

Exploring fp-ts, building off the previous application to create a more oure network connection.

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