Skip to content

Instantly share code, notes, and snippets.

@danecando
Last active July 10, 2022 01:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danecando/e4977845fde8ccfe5c7424179fdc2fea to your computer and use it in GitHub Desktop.
Save danecando/e4977845fde8ccfe5c7424179fdc2fea to your computer and use it in GitHub Desktop.
React hook for uploading files with Firebase JS client API
import type {
StorageReference,
UploadMetadata,
UploadTask,
} from 'firebase/storage';
import * as React from 'react';
import { ref, uploadBytesResumable, getDownloadURL } from 'firebase/storage';
import { getStorage } from '~/services/firebase';
export enum UploadState {
IDLE = 'IDLE',
PAUSED = 'PAUSED',
UPLOADING = 'UPLOADING',
CANCELED = 'CANCELED',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
}
enum ActionType {
SET_UPLOAD_ERROR,
SET_UPLOAD_PROGRESS,
SET_UPLOAD_COMPLETED,
START_UPLOAD,
PAUSE_UPLOAD,
RESUME_UPLOAD,
CANCEL_UPLOAD,
INITIALIZE,
RESET,
}
type UploaderState = {
progress: number;
state: UploadState;
fileUrl?: string;
error?: Error;
_file?: {
file: File;
metadata?: UploadMetadata;
};
_uploadTask?: UploadTask;
storageRef?: StorageReference;
};
type UploaderAction =
| {
type: ActionType.SET_UPLOAD_PROGRESS;
payload: { progress: number };
}
| {
type: ActionType.SET_UPLOAD_ERROR;
payload: { error: Error };
}
| {
type: ActionType.SET_UPLOAD_COMPLETED;
payload: { fileUrl: string };
}
| {
type: ActionType.INITIALIZE;
payload: { file: File; metadata?: UploadMetadata };
}
| {
type: ActionType.START_UPLOAD;
payload: { uploadTask: UploadTask; storageRef: StorageReference };
}
| {
type: ActionType.PAUSE_UPLOAD;
}
| {
type: ActionType.RESUME_UPLOAD;
}
| {
type: ActionType.CANCEL_UPLOAD;
}
| {
type: ActionType.RESET;
};
const initialState = {
progress: 0,
state: UploadState.IDLE,
fileUrl: undefined,
error: undefined,
_file: undefined,
_uploadTask: undefined,
storageRef: undefined,
};
function reducer(state: UploaderState, action: UploaderAction): UploaderState {
switch (action.type) {
case ActionType.INITIALIZE:
return {
...state,
_file: {
file: action.payload.file,
metadata: action.payload.metadata,
},
};
case ActionType.START_UPLOAD:
return {
...state,
progress: 0,
state: UploadState.UPLOADING,
_uploadTask: action.payload.uploadTask,
storageRef: action.payload.storageRef,
};
case ActionType.PAUSE_UPLOAD:
return {
...state,
state: UploadState.PAUSED,
};
case ActionType.RESUME_UPLOAD:
return {
...state,
state: UploadState.UPLOADING,
};
case ActionType.CANCEL_UPLOAD:
return {
...state,
state: UploadState.CANCELED,
};
case ActionType.SET_UPLOAD_PROGRESS:
return {
...state,
progress: action.payload.progress,
};
case ActionType.SET_UPLOAD_ERROR:
return {
...state,
state: UploadState.FAILED,
error: action.payload.error,
};
case ActionType.SET_UPLOAD_COMPLETED:
return {
...state,
progress: 100,
state: UploadState.COMPLETED,
fileUrl: action.payload.fileUrl,
};
case ActionType.RESET:
return initialState;
default:
throw new Error('Unhandled action type');
}
}
export function useFirebaseUploader(path?: string) {
const [
{ progress, state, error, fileUrl, storageRef, _file, _uploadTask },
dispatch,
] = React.useReducer(reducer, initialState);
React.useEffect(() => {
if (_file && path) {
const storage = getStorage();
const storageRef = ref(storage, path);
const uploadTask = uploadBytesResumable(
storageRef,
_file.file,
_file.metadata
);
dispatch({
type: ActionType.START_UPLOAD,
payload: { uploadTask, storageRef },
});
const unsubscribe = uploadTask.on(
'state_changed',
(snapshot) => {
const progress = Math.floor(
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
);
if (snapshot.state === 'running') {
dispatch({
type: ActionType.SET_UPLOAD_PROGRESS,
payload: { progress },
});
}
},
(error) => {
switch (error.code) {
case 'storage/unauthorized':
dispatch({
type: ActionType.SET_UPLOAD_ERROR,
payload: {
error: new Error(
'User does not have permission to access the storage object'
),
},
});
break;
case 'storage/canceled':
break;
case 'storage/unknown':
default:
dispatch({
type: ActionType.SET_UPLOAD_ERROR,
payload: {
error: new Error('Upload failed due to an unknown error'),
},
});
break;
}
unsubscribe();
},
() => {
getDownloadURL(uploadTask.snapshot.ref).then(_completeDownload);
unsubscribe();
}
);
return () => {
unsubscribe();
};
}
}, [_file, path]);
const reset = React.useCallback(() => {
dispatch({ type: ActionType.RESET });
}, []);
const start = React.useCallback(
(file: File, metadata?: UploadMetadata) => {
reset();
dispatch({
type: ActionType.INITIALIZE,
payload: { file, metadata },
});
},
[reset]
);
const pause = React.useCallback(() => {
if (_uploadTask) {
_uploadTask.pause();
dispatch({ type: ActionType.PAUSE_UPLOAD });
}
}, [_uploadTask]);
const resume = React.useCallback(() => {
if (_uploadTask) {
_uploadTask.resume();
dispatch({ type: ActionType.RESUME_UPLOAD });
}
}, [_uploadTask]);
const cancel = React.useCallback(() => {
if (_uploadTask) {
_uploadTask.cancel();
dispatch({ type: ActionType.CANCEL_UPLOAD });
}
}, [_uploadTask]);
const _completeDownload = (fileUrl: string) => {
dispatch({
type: ActionType.SET_UPLOAD_COMPLETED,
payload: { fileUrl },
});
};
return {
progress,
state,
fileUrl,
error,
storageRef,
start,
pause,
resume,
cancel,
reset,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment