Recently I blogged about a new library I released called - react-uploady.
That post can be found here:
react-uploady: uploading files in React
Yoav Niran γ» May 6 '20 γ» 3 min read
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:
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",
};
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;
}
}
`;
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>
);
};
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>
);
});
This component makes use of 3 different hooks:
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.
useItemAbortListener - called when an item is aborted. We use it to set the item status to aborted.
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>);
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 -
Top comments (0)