@rpldy/uploady
TypeScript icon, indicating that this package has built-in type declarations

1.8.0 • Public • Published
npm version circleci status codecov status bundlephobia badge rpldy storybook
Contents

Uploady

This is the main UI package. Its role is to initialize and expose the uploader functionality. It contains the Provider that all other UI packages rely on.

It provides multiple hooks that enable more advanced features and data for client apps.

The best place to get started is at our: React-Uploady Documentation Website

Installation

#Yarn: 
   $ yarn add @rpldy/uploady

#NPM:
   $ npm i @rpldy/uploady

Props

Name (* = mandatory) Type Default Description
Uploader Options
autoUpload boolean true automatically upload files when they are added
destination Destination undefined configure the end-point to upload to
inputFieldName string "file" name (attribute) of the file input field (requires sendWithFormData = true)
grouped boolean false group multiple files in a single request
maxGroupSize number 5 maximum of files to group together in a single request
formatGroupParamName (number, string) => string undefined determine the upload request field name when more than file is grouped in a single upload
fileFilter (File | string, index: number, File[] | string[]) => boolean undefined return false to exclude from batch
method string "POST" HTTP method in upload request
params Object undefined collection of params to pass along with the upload (requires sendWithFormData = true)
forceJsonResponse boolean false parse server response as JSON even if no JSON content-type header received
withCredentials boolean false set XHR withCredentials to true
enhancer UploaderEnhancer undefined uploader enhancer function
concurrent boolean false issue multiple upload requests simultaneously
maxConcurrent number 2 maximum allowed simultaneous requests
send SendMethod @rpldy/sender how to send files to the server
sendWithFormData boolean true upload is sent as part of formdata - when true, additional params can be sent along with uploaded data
formatServerResponse FormatServerResponseMethod undefined function to create the batch item's uploadResponse from the raw xhr response
clearPendingOnAdd boolean false whether to clear pending batch(es) when a new one is added
isSuccessfulCall IsSuccessfulCall undefined callback to use to decide whether upload response is succssful or not
fastAbortThreshold number 100 the pending/active item count threshold from which to start using the performant abort mechanism
userData any undefined metadata set by the user and isn't used by the upload process in any way, provided as a convenience to pass data around
Uploady Options
debug boolean false enable console logs from uploady packages
listeners Object undefined map of event name and event handler
customInput boolean false whether to use a custom file input (see: useFileInput
inputFieldContainer HTMLElement document.body html element to place the file input element inside
children React.Node undefined any part of your React app that will require access to the upload flow (components, hooks, etc.)
capture string null input/file#capture - affects file input only. for example, drag&drop or programmatic uploads will not be affected by this setting
multiple boolean true input/file#multiple - affects file input only. for example, drag&drop or programmatic uploads will not be affected by this setting
accept string null input/file#accept - affects file input only. for example, drag&drop or programmatic uploads will not be affected by this setting
webkitdirectory boolean false webkitdirectory - affects file input only. for example, drag&drop or programmatic uploads will not be affected by this setting
fileInputId string undefined the value to use for the internal file input element
noPortal boolean false Dont render Uploady's file input in a portal. (default: false) For SSR, noPortal = false causes a React warning in DEV.

Example

To be able to use one of the UI Components or one of the hooks, you need to wrap them with Uploady. This will give them access to the UploadyContext.

import Uploady from "@rpldy/uploady";

const App = () => (<Uploady
    multiple
    grouped
    maxGroupSize={2}
    method="PUT"
    destination={{url: "https://my-server", headers: {"x-custom": "123"}}}>

    <RestOfMyApp/>
</Uploady>)

Context

When working in React, The UploadyContext is the API provider for the uploader mechanism. It wraps the uploader and exposes everything the app using it needs.

import React from "react";
import Uploady, { useUploady } from "@rpldy/uploady";

const MyComponent = () => { 
    const uploady = useUploady();

    const onClick = ()=> {
            uploady.showFileUpload();
        };

    return <button onClick={onClick}>Custom Upload Button</button>
};

const App = () => (<Uploady>
    <MyComponent/>
</Uploady>);

UploadyContext API

showFileUpload

(?UploadOptions) => void

Show the native file selection dialog. Optionally Pass options as a parameter to override options set as props on the Uploady component.

upload

(files: UploadInfo | UploadInfo[], addOptions: ?UploadOptions) => void

Upload file(s). Optionally Pass options as the second parameter to override options set as props on the Uploady component.

processPending

(uploadOptions?: UploadOptions) => void

Start uploading batches that were added with autoUpload = false

Upload Options can be added here to be (deep) merged with the options the batch(es) was added with.

clearPending

() => void

Remove all batches that were added with autoUpload = false, and were not uploaded yet.

setOptions

(UploadOptions) => void

Update the uploader instance with different options than the ones used to initialize

getOptions

() => UploadOptions

get the current options used by the uploader

getExtension

(name: any) => ?Object

get an extension registered by that name (through an enhancer)

abort

(id?: string) => void

abort all files being uploaded or a single item by its ID

abortBatch

(id: string) => void

abort a specific batch by its ID

on

(name: any, cb: EventCallback) => OffMethod

register for an event

once

(name: any, cb: EventCallback) => OffMethod

register once for an event

off

(name: any, cb?: EventCallback) => void

unregister an event handler

Hooks

Uploady provides hooks for all uploader events, as well as a few other useful ones.

useBatchAddListener (event hook)

Called when a new batch is added.

This event is cancellable

    import { useBatchAddListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useBatchAddListener((batch, options) => {
            console.log(`batch ${batch.id} was just added with ${batch.items.length} items`);  
        });

        //...    
    };

useBatchStartListener (event hook)

Called when batch items start uploading

This event is cancellable

This event can be scoped to a specific batch by passing the batch id as a second parameter

The callback function can be async or return a promise when needed

    import { useBatchStartListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useBatchStartListener((batch) => {
            console.log(`batch ${batch.id} started uploading`);  
        });

        //or scoped:
        useBatchStartListener((batch) => {
            console.log(`batch ${batch.id} started uploading`);  
        }, "b-123");
        //...    
    };

The callback passed to the hook may also return an object containing items and/or options in order to update the request dynamically, similar to useRequestPreSend only for the entire batch. See withBatchStartUpdate HOC below for more details.

useBatchProgressListener (event hook)

Called every time progress data is received from the upload request(s)

This event can be scoped to a specific batch by passing the batch id as a second parameter

    import { useBatchProgressListener } from "@rpldy/uploady";

   const MyComponent = () => {
        const batch = useBatchProgressListener((batch) => {});
    
        console.log(`batch ${batch.id} is ${batch.completed}% done and ${batch.loaded} bytes uploaded`)

       //...    
   };

Scoping to an id can be done like so:

    //...
    const { completed: batchCompleted } = useBatchProgressListener("batch-id") || { completed: 0 };
    //...

useBatchFinishListener (event hook)

Called when batch items finished uploading

This event can be scoped to a specific batch by passing the batch id as a second parameter

    import { useBatchFinishListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useBatchFinishListener((batch) => {
            console.log(`batch ${batch.id} finished uploading`);  
        });

        //...    
    };

useBatchCancelledListener (event hook)

Called in case batch was cancelled from BATCH_START event handler

This event can be scoped to a specific batch by passing the batch id as a second parameter

    import { useBatchCancelledListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useBatchCancelledListener((batch) => {
            console.log(`batch ${batch.id} was cancelled`);  
        });

        //...    
    };

useBatchAbortListener (event hook)

Called in case abortBatch was called

This event can be scoped to a specific batch by passing the batch id as a second parameter

    import { useBatchAbortListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useBatchAbortListener((batch) => {
            console.log(`batch ${batch.id} was aborted`);  
        });

        //...    
    };

useBatchErrorListener (event hook)

Called in case batch failed with an error. These errors will most likely occur due to invalid event handling. For instance, by a handler (ex: BATCH_START) throwing an error.

This event can be scoped to a specific batch by passing the batch id as a second parameter

    import { useBatchErrorListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useBatchErrorListener((batch) => {
            console.log(`batch ${batch.id} had an error: ${batch.additionalInfo}`);  
        });

        //...    
    };

useBatchFinalizeListener (event hook)

Called for batch when all its items have finished uploading or in case the batch was cancelled(abort) or had an error

This event can be scoped to a specific batch by passing the batch id as a second parameter

    import { useBatchFinalizeListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useBatchFinalizeListener((batch) => {
            console.log(`batch ${batch.id} finished uploading with status: ${batch.state}`);  
        });

        //...    
    };

useItemStartListener (event hook)

Called when item starts uploading (just before) For grouped uploads (multiple files in same xhr request) ITEM_START is triggered for each item separately

This event is cancellable

This event can be scoped to a specific item by passing the item id as a second parameter

The callback function can be async or return a promise when needed

    import { useItemStartListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useItemStartListener((item) => {
            console.log(`item ${item.id} started uploading`);  
        });
    
        //or scoped:
        useItemStartListener((item) => {
            console.log(`item ${item.id} started uploading`);  
        }, "i-123");

        //...    
    };

useItemFinishListener (event hook)

Called when item finished uploading

This event can be scoped to a specific item by passing the item id as a second parameter

    import { useItemFinishListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useItemFinishListener((item) => {
            console.log(`item ${item.id} finished uploading, response was: `, item.uploadResponse, item.uploadStatus);  
        });

        //...    
    };

useItemProgressListener (event hook)

Called every time progress data is received for this file upload

This event can be scoped to a specific item by passing the item id as a second parameter

    import { useItemProgressListener } from "@rpldy/uploady";

    const MyComponent = () => {
        const item = useItemProgressListener((item) => {
        	//callback is optional for this hook
        });
		
        console.log(`item ${item.id} is ${item.completed}% done and ${item.loaded} bytes uploaded`)
    
       //...    
    };

Scoping to an id can be done like so:

    //...
    const { completed: itemCompleted } = useItemProgressListener("item-id") || { completed: 0 };
    //...

useItemCancelListener (event hook)

Called in case item was cancelled from ITEM_START event handler

This event can be scoped to a specific item by passing the item id as a second parameter

    import { useItemCancelListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useItemCancelListener((item) => {
            console.log(`item ${item.id} was cancelled`);  
        });

        //...    
    };

useItemErrorListener (event hook)

Called in case item upload failed

This event can be scoped to a specific item by passing the item id as a second parameter

    import { useItemErrorListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useItemErrorListener((item) => {
            console.log(`item ${item.id} failed - `, item.uploadResponse);  
        });

        //...    
    };

useItemAbortListener (event hook)

Called in case abort was called for an item

This event can be scoped to a specific item by passing the item id as a second parameter

    import { useItemAbortListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useItemAbortListener((item) => {
            console.log(`item ${item.id} was aborted`);  
        });

        //...    
    };

useItemFinalizeListener (event hook)

Called for item when uploading is done due to: finished, error, cancel or abort

This event can be scoped to a specific item by passing the item id as a second parameter

    import { useItemFinalizeListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useItemFinalizeListener((item) => {
            console.log(`item ${item.id} is done with state: ${item.state}`);  
        });

        //...    
    };

useRequestPreSend (event hook)

Called before a group of items is going to be uploaded Group will contain a single item unless "grouped" option is set to true.

Handler receives the item(s) in the group and the upload options that were used. The handler can change data inside the items and in the options by returning different data than received. See simple example below or this more detailed guide.

This event is cancellable

    import { useRequestPreSend } from "@rpldy/uploady";

    const MyComponent = () => {
        useRequestPreSend(({ items, options }) => {        	
            let method = options.method;

            if (options.destination.url.startsWith("https://put-server")) {
                method = "PUT";
            }            

            return {
                options: { method } //will be merged with the rest of the options 
            };  
        });

        //...    
    };

useAllAbortListener (event hook)

Called in case abort was called for all running uploads

    import { useAllAbortListener } from "@rpldy/uploady";

    const MyComponent = () => {
        useAllAbortListener(() => {
            console.log("abort all was called");
        });
    };

useUploadyContext (alias: useUploady)

Shortcut hook to get the Uploady Context instance

Will throw in case used outside of Uploady render tree

    import { useUploady } from "@rpldy/uploady";

    const MyComponent = () => {
        const uploady = useUploady();
        
        const onClick = () => {
            uploady.showFileUpload();
        }

        //...       
    };

    const App = () => (
        <Uploady destination={{...}}>
            <MyComponent/>
        </Uploady>
    );

useUploadOptions

Shortcut hook to set/get upload options.

    import { useUploadOptions } from "@rpldy/uploady";

    const MyComponent = () => {
        const options = useUploadOptions({ grouped: true, maxGroupSize: 3 });
        
        //...       
    };

useAbortItem

Returns abort item method

    import { useAbortItem } from "@rpldy/uploady";
    
    const MyComponent = () => {
        const abortItem = useAbortItem();
        
        return <button onClick={() => abortItem("i-123")}>Abort Item</button>       
    };

useAbortBatch

Returns abort batch method

    import { useAbortBatch } from "@rpldy/uploady";
    
    const MyComponent = () => {
        const abortBatch = useAbortBatch();
        
        return <button onClick={() => abortBatch("b-123")}>Abort Batch</button>       
    };

useAbortAll

Returns abort all method

  import { useAbortAll } from "@rpldy/uploady";
  
  const MyComponent = () => {
      const abortAll = useAbortAll();
      
      return <button onClick={() => abortAll()}>Abort All</button>       
  };

useFileInput

When customInput prop is set to true, Uploady will not create its own file input element. In this case, Uploady will wait for a ref to an existing input.

The way you pass in your own input element is by using this hook.

In case Uploady wasn't provided with a destination prop or if it doesn't have a URL property, Uploady will check whether the input resides in a form. It will then use the form's action and method to set the upload endpoint and request method.

In case the form's attributes were used for the upload destination, updating the form's attributes dynamically won't affect the uploader configuration once it was set.

import Uploady, { useFileInput } from "@rpldy/uploady";
import UploadButton from "@rpldy/upload-button";

const MyForm = () => {
    const inputRef = useRef();
    useFileInput(inputRef);

    return <form action="/upload" method="POST">
        <input type="file" name="testFile" style={{ display: "none" }} ref={inputRef}/>
    </form>;
};

export const WithCustomFileInputAndForm = () => {
    return <section>
        <Uploady
            debug
            customInput
        >
            <MyForm />
            <UploadButton/>
        </Uploady>
    </section>
};

This hook can also be used to retrieve Uploady's internal file input. Calling the hook without parameters will return the ref.

const inputRef = useFileInput();

if (inputRef.current) {
    inputRef.current.setAttribute("webkitdirectory", "true");
}

NOTE! This isn't the recommended, or the 'Reacty' way to do things. It is still recommended to pass along a ref to an input that you render. In the future, accessing the internal input may have other consequences related to opting to interact with it directly instead of passing props to the Uploady component.

Check out the Custom Input guide for more details and examples.

HOCs

withRequestPreSendUpdate

HOC to enable components to interact with the upload data and options just-in-time before the request is sent. This is a hatch point to introduce custom logic that may affect the upload data.

A good example use-case for this is applying crop to selected image before it is uploaded.

When rendering the HOC's output, the id of the batch-item must be provided as a prop. This ensures the HOC only re-renders for a specific item and not for all. The id of the batch-item can be obtained from a hook (ex: useItemStartListener or useBatchStartListener)

    import React, { useState, useCallback } from "react";
    import Cropper from "react-easy-crop";
	import Uploady, { withRequestPreSendUpdate } from "@rpldy/uploady";
	import UploadButton from "@rpldy/upload-button";
    import cropImage from "./my-image-crop-code";

    const ItemCrop = withRequestPreSendUpdate((props) => {
        const [crop, setCrop] = useState({ x: 0, y: 0 });
        const [cropPixels, setCropPixels] = useState(null);
        
        const { url, updateRequest, requestData } = props;
         
        const onUploadCrop = useCallback(async() => {
            if (updateRequest && cropPixels) {
                //replace the file data with the cropped result
                requestData.items[0].file = await cropImage(url, requestData.items[0].file, cropPixels);
                //resume the upload flow with the updated file data			    
                updateRequest({ items: requestData.items });
            }
        }, [url, requestData, updateRequest, cropPixels]);
    
        const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => {
            setCropPixels(croppedAreaPixels);
        }, []);

        return <>            
            <Cropper
                image={url}
                crop={crop}
                onCropChange={setCrop}
                onCropComplete={onCropComplete}
            />           
            <button style={{ display: updateRequest && cropPixels ? "block" : "none" }}
                    onClick={onUploadCrop}>
                Upload Cropped
            </button>
        </>;
    });

    const MyApp = () => {
        return <Uploady destination={{ url: "my-server.com/upload" }}>
            <UploadButton />
            <ItemCrop id="batch-item-1" />
        </Uploady>
    }

See the Crop Guide for a full example.

withBatchStartUpdate

HOC to enable components to interact with the upload data and options of the batch just-in-time before the items are processed and requests are being sent.

This makes it possible to create a UI that will allow the user to interact and possible make changes to different or all items within the batch before a single request is made. For example: cropping multiple items prior to upload.

When rendering the HOC's output, the id of the batch must be provided as a prop. The id of the batch can be obtained from the useBatchAddListener

    import React, { useState, useCallback } from "react";
    import Cropper from "react-easy-crop";
    import Uploady, { withBatchStartUpdate } from "@rpldy/uploady";
    import UploadButton from "@rpldy/upload-button";
    import UploadPreview from "@rpldy/upload-preview";
    import cropImage from "./my-image-crop-code";
    
    const CropperForMultiCrop = ({ item, url, setCropForItem }) => {
        const [crop, setCrop] = useState({ x: 0, y: 0 });
        const [cropPixels, setCropPixels] = useState(null);
		
        const onSaveCrop = async () => {
            const cropped = await cropImage(url, item.file, cropPixels);
            setCropForItem(item.id, cropped);
        };

        const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => {
            setCropPixels(croppedAreaPixels);
        }, []);
		
        return (<div>
            <Cropper
                image={url}
                crop={crop}
                onCropChange={setCrop}
                onCropComplete={onCropComplete}
            />
            {cropPixels && 
                <Button onClick={onSaveCrop} id="save-crop-btn">Save Crop</Button>}
        </div>);
    };

    const BatchCrop = withBatchStartUpdate((props) => {
	    const { id, updateRequest, requestData } = props;
        const [cropped, setCropped] = useState({});
        const hasData = !!(id && requestData);
		
        const setCropForItem = (id, data) => {
            setCropped((cropped) => ({ ...cropped, [id]: data }));
        };
		
        const onUploadAll = () => {
            if (updateRequest) {
                const readyItems = requestData.items
                    .map((item) => {
                        item.file = cropped[item.id] || item.file;
                        return item;
                    });

				//update the items in the batch with the cropped files
                updateRequest({ items: readyItems });
            }
        };

        const getPreviewCompProps = useCallback((item) => {
            return ({
                onPreviewSelected: setSelected,
                isCroppedSet: cropped[item.id],
            });
        }, [cropped, setSelected]);

        return (<div>
            {hasData &&
                <button onClick={onUploadAll}>Upload All</button>}

            <UploadPreview
                rememberPreviousBatches
                PreviewComponent={ItemPreviewThumb}
                fallbackUrl="https://icon-library.net/images/image-placeholder-icon/image-placeholder-icon-6.jpg"
                previewComponentProps={getPreviewCompProps}
            />

            {selectedItem && hasData &&
                <CropperForMultiCrop
                    {...selected}
                    item={selectedItem}
                    setCropForItem={setCropForItem}
                />}
        </div>);
    });
	
    const MultiCropQueue = () => {
        const [currentBatch, setCurrentBatch] = useState(null);
    
        useBatchAddListener((batch) => setCurrentBatch(batch.id));
    
        return <BatchCrop id={currentBatch} />;
    };
	
    export const MyApp = () => {
        return <Uploady destination={{ url: "my-server.com/upload" }}>
            <UploadButton />
            <MultiCropQueue  />
        </Uploady>;
    };

See the Multi Crop Guide for a full example.

Contribute

Show Uploady your support by giving us a .

If you'd like to help Uploady grow & improve, take a look at the Contributing doc.

The Discussions page is a great place to ask questions, raise ideas and interact with Uploady maintainer, users and contributors.

Already using Uploady in Production? Let us know how & where in this open discussion.

Financial Contributors

Want to help sustain and grow Uploday? You can become a financial backer on OpenCollective.

Become a financial contributor and help us sustain our community.

You can make a one-time contribution or on a monthly basis

Package Sidebar

Install

npm i @rpldy/uploady

Weekly Downloads

10,425

Version

1.8.0

License

MIT

Unpacked Size

191 kB

Total Files

16

Last publish

Collaborators

  • yoavniran