DEV Community

loading...
Cover image for hooked on file uploads with react-uploady

hooked on file uploads with react-uploady

poeticgeek profile image Yoav Niran Updated on ・5 min read

Recently I blogged about a new library I released called - react-uploady.

That post can be found here:

One of the cool things about RU is that it offers a lot of hooks for managing different aspects of the upload process within your app.

In this post, I'd like to show a few of these hooks in practice and demonstrate how useful they can be by saving on code you need to write yourself.

This demo will take you through building a simple upload queue UI with progress indication per file, a preview image, abort button, and retry button. Ok, maybe not so simple after all. But the code is! :)

The full working code for the demo can be found in this sandbox.

It looks and behaves like this:

Alt Text

Below is the full code and explanation on how to get this result.

Let's start

First of, we import what we need and define a few constants we'll use to mark the status of the items in the queue:

import React, { useCallback, useState, memo } from "react";
import styled from "styled-components";
import { Circle } from "rc-progress";
import Uploady, {
    useItemProgressListener,
    useItemAbortListener,
    useItemErrorListener,
    useAbortItem,
} from "@rpldy/uploady";
import UploadButton from "@rpldy/upload-button";
import UploadPreview from "@rpldy/upload-preview";
import retryEnhancer, { useRetry } from "@rpldy/retry-hooks";

const STATES = {
    PROGRESS: "PROGRESS",
    DONE: "DONE",
    ABORTED: "ABORTED",
    ERROR: "ERROR",
};

const STATE_COLORS = {
    [STATES.PROGRESS]: "#f4e4a4",
    [STATES.DONE]: "#a5f7b3",
    [STATES.ABORTED]: "#f7cdcd",
    [STATES.ERROR]: "#ee4c4c",
};

Enter fullscreen mode Exit fullscreen mode

Basic UI Components

Then we define a few styled components. These will be used to wrap different elements in the queue UI:


const StyledCircle = styled(Circle)`
  width: 32px;
  height: 32px;
`;

const PreviewsContainer = styled.div`
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  border-top: 1px solid #0c86c1;
  margin-top: 10px;
`;

const PreviewImageWrapper = styled.div`
  height: 150px;
  text-align: center;
  width: 100%;
`;

const PreviewImage = styled.img`
  max-width: 200px;
  height: auto;
  max-height: 140px;
`;

const PreviewItemContainer = styled.div`
  width: 220px;
  padding: 10px;
  display: flex;
  flex-direction: column;
  box-shadow: ${({ state }) => (state ? STATE_COLORS[state] : "#c3d2dd")} 0px
    8px 5px -2px;
  position: relative;
  align-items: center;
  margin: 0 10px 10px 0;
`;

const ImageName = styled.span`
  position: absolute;
  top: 10px;
  font-size: 12px;
  padding: 3px;
  background-color: #25455bab;
  width: 180px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: #fff;
`;

const PreviewItemBar = styled.div`
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 0;
  width: 100%;
  box-shadow: #5dbdec 0px -3px 2px -2px;
`;

const ItemButtons = styled.div`
  button {
    width: 52px;
    height: 34px;
    font-size: 26px;
    line-height: 26px;
    cursor: pointer;
    margin-right: 4px;

    :disabled {
      cursor: not-allowed;
      background-color: grey;
      color: grey;
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

Most of the above is fairly basic. The only thing really worth mentioning is that we give the item container a different colored box shadow based on its status. Green for finished, Red for error, etc.

Finally, some hooks!

Next, we'll define the abort and retry buttons:


const AbortButton = ({ id, state }) => {
    const abortItem = useAbortItem();
    const onAbort = useCallback(() => abortItem(id), [id, abortItem]);

    return (
        <button
            disabled={state === STATES.ABORTED || state === STATES.DONE}
            onClick={onAbort}
        >
            🛑
        </button>
    );
};

const RetryButton = ({ id, state }) => {
    const retry = useRetry();
    const onRetry = useCallback(() => retry(id), [id, retry]);

    return (
        <button disabled={state !== STATES.ABORTED} onClick={onRetry}>
            🔃
        </button>
    );
};
Enter fullscreen mode Exit fullscreen mode

Here we start making use of some RU hooks. useAbortItem and useRetry. Both provide functions we can call to affect our uploads. The former to abort a pending or uploading request; the latter to retry a failed upload either due to server error, timeout or an abort.

Both functions accept the item id to be able to do their job. We'll shortly see where we can get this id from.

Showing the Preview

Next, we define the QueueItem component which will show the item with its preview and buttons within the queue:


const QueueItem = memo((props) => {
    const [progress, setProgress] = useState(0);
    const [itemState, setItemState] = useState(0);

    useItemProgressListener(item => {
        if (item.completed > progress) {
            setProgress(() => item.completed);
            setItemState(() =>
                item.completed === 100 ? STATES.DONE : STATES.PROGRESS
            );
        }
    }, props.id);

    useItemAbortListener((item) => {
        setItemState(STATES.ABORTED);
    }, props.id);

    useItemErrorListener((item) =>{
        setItemState(STATES.ERROR);
    }, props.id);

    return (
        <PreviewItemContainer state={itemState}>
            <ImageName>{props.name}</ImageName>
            <PreviewImageWrapper>
                <PreviewImage src={props.url} />
            </PreviewImageWrapper>
            <PreviewItemBar>
                <ItemButtons>
                    <AbortButton id={props.id} state={itemState} />
                    <RetryButton id={props.id} state={itemState} />
                </ItemButtons>
                <StyledCircle
                    strokeWidth={4}
                    percent={progress}
                    strokeColor={progress === 100 ? "#00a626" : "#2db7f5"}
                />
            </PreviewItemBar>
        </PreviewItemContainer>
    );
});
Enter fullscreen mode Exit fullscreen mode

This component makes use of 3 different hooks:

  1. useItemProgressListener - called when there is new progress information about the file being uploaded. It receives the item with the progress percentage of the upload and how many bytes were uploaded.

  2. useItemAbortListener - called when an item is aborted. We use it to set the item status to aborted.

  3. useItemErrorListener - called when an item upload encounters an error. We use it to set the item status to error.

Important to note that all 3 hooks use the scoping param by passing the item id as the 2nd argument to the hook function.
This ensures they will only be called for this one specific item.

QueueItem is getting the item id (and preview URL) from the parent that renders it. This being the UploadPreview component from RU. Below we see how its used.

The rest is just using the previously defined styled components to create the UI for the queue item.

Wrap it up

Finally, we put everything together with the help of a few of the components React-Uploady provides:


export const MyApp = () => (
        <Uploady
            destination={{url: "my-server.com/upload"}}
            enhancer={retryEnhancer}
        >           
                <UploadButton>Upload Files</UploadButton>

                <PreviewsContainer>
                    <UploadPreview
                        rememberPreviousBatches
                        PreviewComponent={QueueItem}
                    />
                </PreviewsContainer>
        </Uploady>);

Enter fullscreen mode Exit fullscreen mode

We wrap our "app" with an Uploady instance, giving it the retryEnhancer from @rpldy/retry-hookd to enable retry capability.

Inside, we add an UploadButton (@rpldy/upload-button) component to initiate uploads by choosing files from the local filesystem.

And then we use the UploadPreview component (@rpldy/upload-preview) and set our QueueItem as the PreviewComponent. This will render our queue item UI for each batch item being uploaded.

We also use the rememberPreviousBatches so previews (QueueItem) are added as more files are selected or when failed items are retried (instead of the queue being replaced each time).

And that's all there is to it.

Check it out -

GitHub logo rpldy / react-uploady

Modern file uploading - components & hooks for React

Discussion

pic
Editor guide