hooked on file uploads with react-uploady
Tags: react, javascript, hooks, fileupload
Published: 2020-06-06 16:37:51

Recently I blogged about a new library I released called - __[react-uploady](https://github.com/rpldy/react-uploady)__.

That post can be found [here](https://dev.to/poeticgeek/react-uploady-uploading-files-in-react-163f): 

{% link https://dev.to/poeticgeek/react-uploady-uploading-files-in-react-163f %}

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](https://codesandbox.io/s/react-uploady-hooks-pjzui?file=/src/App.js).

{% codesandbox react-uploady-hooks-pjzui module=/src/App.js %}

It looks and behaves like this:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ip1kjh25u1xv2r3yj03g.gif)

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:

```javascript
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",
};

```

### Basic UI Components

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

```javascript

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

> 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:


```javascript

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

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:

```javascript

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

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:

```javascript

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

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

```

We wrap our "app" with an Uploady instance, giving it the _retryEnhancer_ from [@rpldy/retry-hookd](https://www.npmjs.com/package/@rpldy/retry-hooks) to enable retry capability.

Inside, we add an UploadButton ([@rpldy/upload-button](https://www.npmjs.com/package/@rpldy/upload-button)) component to initiate uploads by choosing files from the local filesystem.

And then we use the UploadPreview component ([@rpldy/upload-preview](https://www.npmjs.com/package/@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 rpldy/react-uploady no-readme %}