Uploading Files in React While Keeping The UI Completely In Sync
Tags: react, javascript, node, webdev
Published: 2019-06-13 05:11:37

Find me on [medium](https://medium.com/@jsmanifest).

Building a file upload component is a very important skill to learn as it allows users to select and send files outside of their local environment.

With that said, this post is putting a heavy focus on the native _file_ api in JavaScript. If you want to know a little more about how the file api works, click [here](https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications).

At first, building a file upload component can be an awkward concept to grasp especially if you want to customize the look and feel of it. (We can go over custom designing file input components in a future tutorial). But once you get a good understanding of the concepts, it actually _isn't that bad_!

I mean, you _can_ just create a file input element, pass in an onChange and call it a day from there. But are you going to take care of your users by presenting them the current _state_ of the process each way? Or are you just going to let them sit there and hope that they see the end of it without any visual updates in between?

What if the user's internet disconnects? What if the server doesn't respond with anything? What if file 8 of 14 is big for them? What if the user was waiting for the upload process to finish for 10 minutes and wanted to see how far it has gotten from there? Or which files have already been uploaded?

You _have_ to keep the user consistently updated with what's going on in the background if you want the UX to stay consistent. A professional, consistent user interface helps to establish trust with your app in a technological point of view. If you plan on having an app where users sign up and pay for some service of yours, they have to _trust the technology_ that you are offering them and that your technology is better than everyone else. You're developing in React, you have all the power to go up and beyond!

## But where do I start?

No worries! This post will teach _you_ how to create a user interface containing a file upload component that will help users select and send their files somewhere, while allowing the interface to tap into _every_ update from the moment of instantiation to the end. Creating the component is one thing, but having the UI in sync with state updates throughout the whole process is a different story.

Let's begin!

In this tutorial we are going to quickly generate a react project with [create-react-app](https://github.com/facebook/create-react-app).

Go ahead and create a project using the command below. For this tutorial i'll call it _file-upload-with-ux_

```bash
npx create-react-app file-upload-with-ux
```

Now go into the directory when it finishes:

```bash
cd file-upload-with-ux
```

The first thing we are going to do is open up `App.js` and replace the default code with our own implementation:

> src/App.js

```jsx
import React from 'react'
import './App.css'

const Input = (props) => (
  <input type='file' name='file-input' multiple {...props} />
)

const App = () => {
  const onSubmit = (e) => {
    e.preventDefault()
  }

  const onChange = (e) => {
    console.log(e.target.files)
  }

  return (
    <div className='container'>
      <form className='form' onSubmit={onSubmit}>
        <div>
          <Input onChange={onChange} />
          <button type='submit'>Submit</button>
        </div>
      </form>
    </div>
  )
}

export default App
```

Here we defined a _form_ element and passed in an _onSubmit_ handler so that we can access all of the files the user selected once they hit submit.

Inside the form, we defined the file input component which will allow the user to select any of their files. The input takes in an _onChange_ handler so we pass that in as well. The onChange handler will be able to receive the files by accessing _e.target.files_ inside the first argument.

I applied some basic styles inside `App.css`. You can choose to use them or skip this step:

> App.css

```css
.container {
  padding: 8px;
  width: 100%;
  box-sizing: border-box;
  overflow-x: hidden;
}

.form {
  position: relative;
  width: 100%;
  height: 100%;
}

.form input,
button {
  margin-bottom: 15px;
}

.form button {
  padding: 8px 17px;
  border: 0;
  color: #fff;
  background: #265265;
  cursor: pointer;
}

.form button:hover {
  background: #1e3d4b;
}
```

So we have a basic component set up with the handlers set in place. We're now going to create a custom react hook so that we can place all of the dirty state logic inside--away from the UI components.

I'm going to call this `useFileHandlers.js`:

> src/useFileHandlers.js

```jsx
import React from 'react'

const initialState = {
  files: [],
  pending: [],
  next: null,
  uploading: false,
  uploaded: {},
  status: 'idle',
}

const useFileHandlers = () => {
  return {}
}

export default useFileHandlers
```

The most important part of this entire post is probably the _initialState_ shown above. This is what will allow the UI to tap into every moment of the file uploading process.

_files_ is where the user initially loads an _array_ of files by selecting them from the file input.

_pending_ will be used to let the UI know _what_ file is currently being processed and _how many_ files are left over.

_next_ will be assigned the next item in the _pending_ array when the code detects that it's ready to do so.

_uploading_ will be used for the code to know that files are still being uploaded.

_uploaded_ will be the object we insert files into as soon as they are done uploading.

And finally, _status_ is provided as extra convenience mainly for the _user interface_ to utilize to its advantage.

We are going to use the _useReducer_ hook api from react because its perfect for our use.

But first, lets define some constants above the _useFileHandlers_ hook so we make sure we don't mistype anything later when applying state updates:

> src/useFileHandlers.js

```jsx
const LOADED = 'LOADED'
const INIT = 'INIT'
const PENDING = 'PENDING'
const FILES_UPLOADED = 'FILES_UPLOADED'
const UPLOAD_ERROR = 'UPLOAD_ERROR'
```

These will go into the _reducer_ that gets passed in as the first argument to _useReducer_.

Now to define the reducer:

> src/useFileHandlers.js

```jsx
const reducer = (state, action) => {
  switch (action.type) {
    default:
      return state
  }
}
```

We probably shouldn't be forgetting about importing _useReducer_ from react now, eh?

> src/useFileHandlers.js

```jsx
import { useReducer } from 'react'
```

Now to define the state/dispatch api into the hook:

> src/useFileHandlers.js

```jsx
const useFileHandlers = () => {
  const [state, dispatch] = useReducer(reducer, initialState)

  return {}
}

export default useFileHandlers
```

Now we're going to go back to the _onChange_ implementation we set earlier and enhance it further.

Let's add a new switch case into the reducer for it before we do that:

> src/useFileHandlers.js

```jsx
const reducer = (state, action) => {
  switch (action.type) {
    case 'load':
      return { ...state, files: action.files, status: LOADED }
    default:
      return state
  }
}
```

This will allow the onChange handler to pass files into the state as soon as it invokes:

> src/useFileHandlers.js

```jsx
const onChange = (e) => {
  if (e.target.files.length) {
    const arrFiles = Array.from(e.target.files)
    const files = arrFiles.map((file, index) => {
      const src = window.URL.createObjectURL(file)
      return { file, id: index, src }
    })
    dispatch({ type: 'load', files })
  }
}
```

Something to note here is that _e.target.files_ isn't an array when we retrieve it from the event object--its a [FileList](https://developer.mozilla.org/en-US/docs/Web/API/FileList).

The reason why we are converting it to an array is so that the UI components can map over them and show useful information like file sizes and file types. Otherwise, the components will make the app crash trying to map over *FileList*s.

So far, here's the entire implementation of our custom hook:

> src/useFileHandlers.js

```jsx
import { useReducer } from 'react'

// Constants
const LOADED = 'LOADED'
const INIT = 'INIT'
const PENDING = 'PENDING'
const FILES_UPLOADED = 'FILES_UPLOADED'
const UPLOAD_ERROR = 'UPLOAD_ERROR'

const initialState = {
  files: [],
  pending: [],
  next: null,
  uploading: false,
  uploaded: {},
  status: 'idle',
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'load':
      return { ...state, files: action.files, status: LOADED }
    default:
      return state
  }
}

const useFileHandlers = () => {
  const [state, dispatch] = useReducer(reducer, initialState)

  const onChange = (e) => {
    if (e.target.files.length) {
      const arrFiles = Array.from(e.target.files)
      const files = arrFiles.map((file, index) => {
        const src = window.URL.createObjectURL(file)
        return { file, id: index, src }
      })
      dispatch({ type: 'load', files })
    }
  }

  return {}
}

export default useFileHandlers
```

Now we're going to focus on the other handler--_onSubmit_. This invokes when the user submits the form (obviously). Inside the _onSubmit_ handler we wrap it with a [_useCallback_](https://reactjs.org/docs/hooks-reference.html#usecallback) so that it will always obtain the latest state values.

> src/useFileHandlers.js

```jsx
import { useCallback, useReducer } from 'react'
```

> src/useFileHandlers.js

```jsx
const onSubmit = useCallback(
  (e) => {
    e.preventDefault()
    if (state.files.length) {
      dispatch({ type: 'submit' })
    } else {
      window.alert("You don't have any files loaded.")
    }
  },
  [state.files.length],
)
```

This onSubmit handler invokes _after_ onChange, so it can just pull in the files from _state.files_ that was just set by _onChange_, to instantiate the upload process.

And to instantiate the upload process, we need another switch case for that:

> src/useFileHandlers.js

```jsx
const reducer = (state, action) => {
  switch (action.type) {
    case 'load':
      return { ...state, files: action.files, status: LOADED }
    case 'submit':
      return { ...state, uploading: true, pending: state.files, status: INIT }
    default:
      return state
  }
}
```

Okay, now here's what happens:

1. It changes _state.uploading_ to true. When you change state.uploading to _true_, you can start wreaking havoc with the UI components and display anything you want to the user as long as they understand that you are trying to convey some message to them that the files are being uploading.

2. It initiates _state.pending_ with all of the files that the user selected. You can go ahead and wreak havoc with the UI components with this too. There are so many ways to use this part of the state. For now however, i'll just skip that part because I want to get through the whole tutorial with you first :)

3. It sets the convenience part of the state, _status_ to _"INIT"_. You can also use this somewhere in the hook or in the UI to trigger some "_onStart_" logic, or whatever you want--since it won't ever come back to this value until a new upload process starts.

Now we'll return the states as well as the onSubmit and onChange handler back out so that the UI can happily access them:

> src/useFileHandlers.js

```jsx
return {
  ...state,
  onSubmit,
  onChange,
}
```

> src/useFileHandlers.js

The next thing we're going to tackle is the _useEffect_ part. We _need_ the useEffect to facilitate a "run-to-completion" functionality.

These useEffects are a pretty important implementation in this tutorial because they are what creates a perfect, consistent synchronized flow between the UI and the custom hook--_everywhere_ as you will see in a bit.

> src/useFileHandlers.js

```jsx
import { useCallback, useEffect, useReducer } from 'react'
```

We're going to define our _first useEffect_ which will be responsible for facilitating the next file to be uploaded as soon as it detects that it is ready to do so (as long as there are still items in _state.pending_):

> src/useFileHandlers.js

```jsx
// Sets the next file when it detects that state.next can be set again
useEffect(() => {
  if (state.pending.length && state.next == null) {
    const next = state.pending[0]
    dispatch({ type: 'next', next })
  }
}, [state.next, state.pending])
```

It grabs the next available file in the _state.pending_ array and creates a signal using _dispatch_, sending the file as the next _state.next_ object:

> src/useFileHandlers.js

```jsx
const reducer = (state, action) => {
  switch (action.type) {
    case 'load':
      return { ...state, files: action.files, status: LOADED }
    case 'submit':
      return { ...state, uploading: true, pending: state.files, status: INIT }
    case 'next':
      return {
        ...state,
        next: action.next,
        status: PENDING,
      }
    default:
      return state
  }
}
```

We added a _status: PENDING_ here as a convenience again. However you choose to handle this part of the upload process is entirely up to you!

This next snippet is going to show a utility function that I provided just to help log to the console for you to see, just for this tutorial.

> src/useFileHandlers.js

```jsx
const logUploadedFile = (num, color = 'green') => {
  const msg = `%cUploaded ${num} files.`
  const style = `color:${color};font-weight:bold;`
  console.log(msg, style)
}
```

The _second useEffect_ we will apply next will be responsible for _uploading_ the _next_ file that was just set in the state:

> src/useFileHandlers.js

```jsx
const countRef = useRef(0)

// Processes the next pending thumbnail when ready
useEffect(() => {
  if (state.pending.length && state.next) {
    const { next } = state
    api
      .uploadFile(next)
      .then(() => {
        const prev = next
        logUploadedFile(++countRef.current)
        const pending = state.pending.slice(1)
        dispatch({ type: 'file-uploaded', prev, pending })
      })
      .catch((error) => {
        console.error(error)
        dispatch({ type: 'set-upload-error', error })
      })
  }
}, [state])
```

Inside the _.then()_ handler I created a new variable _prev_ and assigned it the _next_ object that finished uploaded. This is just for readability purposes because we don't want to be confused in the switch cases, as we will see in a bit.

You might have noticed that there was a _useRef_ snuck in there. Yes, I confess. I did it. But the reason why I did it is because we're going to use and mutate it for the _logUploadedFile_ utility function that I provided.

> src/useFileHandlers.js

```jsx
import { useCallback, useEffect, useReducer, useRef } from 'react'
```

Oh, and if you need some mock function to simulate an "upload" promise handler as seen in the snippet, you can use this:

```jsx
const api = {
  uploadFile({ timeout = 550 ) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve()
      }, timeout)
    })
  },
}
```

Now go ahead and update your reducer by applying the _'file-uploaded'_ and _'set-upload-error'_ switch cases:

> src/useFileHandlers.js

```jsx
const reducer = (state, action) => {
  switch (action.type) {
    case 'load':
      return { ...state, files: action.files, status: LOADED }
    case 'submit':
      return { ...state, uploading: true, pending: state.files, status: INIT }
    case 'next':
      return {
        ...state,
        next: action.next,
        status: PENDING,
      }
    case 'file-uploaded':
      return {
        ...state,
        next: null,
        pending: action.pending,
        uploaded: {
          ...state.uploaded,
          [action.prev.id]: action.prev.file,
        },
      }
    case 'set-upload-error':
      return { ...state, uploadError: action.error, status: UPLOAD_ERROR }
    default:
      return state
  }
}
```

For the _file-uploaded_ case, we reset _next_ back to _null_ so that the _first useEffect_ can respond to it again. When it does, it will pull in the next file in the _state.pending_ queue and assign that to the next _state.next_ value. You can already start to see how this becomes a self-running process--a _run to completion_ implementation!

Anywho, we apply the file that was just uploaded to the _state.uploaded_ object so that the UI can use this to their advantage as well. This is also a really useful feature in this tutorial because if you were rendering a bunch of thumbnails you can shade each row out after they're uploaded on the fly! :) Screenshots at the end of this post.

The _third useEffect_ will be responsible for shutting down the upload process by sending a _files-uploaded_ signal to the reducer:

> src/useFileHandlers.js

```jsx
// Ends the upload process
useEffect(() => {
  if (!state.pending.length && state.uploading) {
    dispatch({ type: 'files-uploaded' })
  }
}, [state.pending.length, state.uploading])
```

Adding that to the reducer looks something like this:

> src/useFileHandlers.js

```jsx
const reducer = (state, action) => {
  switch (action.type) {
    case 'load':
      return { ...state, files: action.files, status: LOADED }
    case 'submit':
      return { ...state, uploading: true, pending: state.files, status: INIT }
    case 'next':
      return {
        ...state,
        next: action.next,
        status: PENDING,
      }
    case 'file-uploaded':
      return {
        ...state,
        next: null,
        pending: action.pending,
        uploaded: {
          ...state.uploaded,
          [action.prev.id]: action.prev.file,
        },
      }
    case 'files-uploaded':
      return { ...state, uploading: false, status: FILES_UPLOADED }
    case 'set-upload-error':
      return { ...state, uploadError: action.error, status: UPLOAD_ERROR }
    default:
      return state
  }
}
```

And we are done with the custom hook! Hurray!

Here is the final code for the custom hook:

> src/useFileHandlers.js

```jsx
import { useCallback, useEffect, useReducer, useRef } from 'react'

const api = {
  uploadFile({ timeout = 550 }) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve()
      }, timeout)
    })
  },
}

const logUploadedFile = (num, color = 'green') => {
  const msg = `%cUploaded ${num} files.`
  const style = `color:${color};font-weight:bold;`
  console.log(msg, style)
}

// Constants
const LOADED = 'LOADED'
const INIT = 'INIT'
const PENDING = 'PENDING'
const FILES_UPLOADED = 'FILES_UPLOADED'
const UPLOAD_ERROR = 'UPLOAD_ERROR'

const initialState = {
  files: [],
  pending: [],
  next: null,
  uploading: false,
  uploaded: {},
  status: 'idle',
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'load':
      return { ...state, files: action.files, status: LOADED }
    case 'submit':
      return { ...state, uploading: true, pending: state.files, status: INIT }
    case 'next':
      return {
        ...state,
        next: action.next,
        status: PENDING,
      }
    case 'file-uploaded':
      return {
        ...state,
        next: null,
        pending: action.pending,
        uploaded: {
          ...state.uploaded,
          [action.prev.id]: action.prev.file,
        },
      }
    case 'files-uploaded':
      return { ...state, uploading: false, status: FILES_UPLOADED }
    case 'set-upload-error':
      return { ...state, uploadError: action.error, status: UPLOAD_ERROR }
    default:
      return state
  }
}

const useFileHandlers = () => {
  const [state, dispatch] = useReducer(reducer, initialState)

  const onSubmit = useCallback(
    (e) => {
      e.preventDefault()
      if (state.files.length) {
        dispatch({ type: 'submit' })
      } else {
        window.alert("You don't have any files loaded.")
      }
    },
    [state.files.length],
  )

  const onChange = (e) => {
    if (e.target.files.length) {
      const arrFiles = Array.from(e.target.files)
      const files = arrFiles.map((file, index) => {
        const src = window.URL.createObjectURL(file)
        return { file, id: index, src }
      })
      dispatch({ type: 'load', files })
    }
  }

  // Sets the next file when it detects that its ready to go
  useEffect(() => {
    if (state.pending.length && state.next == null) {
      const next = state.pending[0]
      dispatch({ type: 'next', next })
    }
  }, [state.next, state.pending])

  const countRef = useRef(0)

  // Processes the next pending thumbnail when ready
  useEffect(() => {
    if (state.pending.length && state.next) {
      const { next } = state
      api
        .uploadFile(next)
        .then(() => {
          const prev = next
          logUploadedFile(++countRef.current)
          const pending = state.pending.slice(1)
          dispatch({ type: 'file-uploaded', prev, pending })
        })
        .catch((error) => {
          console.error(error)
          dispatch({ type: 'set-upload-error', error })
        })
    }
  }, [state])

  // Ends the upload process
  useEffect(() => {
    if (!state.pending.length && state.uploading) {
      dispatch({ type: 'files-uploaded' })
    }
  }, [state.pending.length, state.uploading])

  return {
    ...state,
    onSubmit,
    onChange,
  }
}

export default useFileHandlers
```

But wait, it's not over yet. We still need to apply this logic to the user interface. Aww shucks!

We'll import the `useFileHandlers` hook and use it in the component. We'll also make the UI map over each file and render them as thumbnails:

> src/App.js

```jsx
import React from 'react'
import useFileHandlers from './useFileHandlers'
import './App.css'

const Input = (props) => (
  <input
    type='file'
    accept='image/*'
    name='img-loader-input'
    multiple
    {...props}
  />
)

const App = () => {
  const {
    files,
    pending,
    next,
    uploading,
    uploaded,
    status,
    onSubmit,
    onChange,
  } = useFileHandlers()

  return (
    <div className='container'>
      <form className='form' onSubmit={onSubmit}>
        <div>
          <Input onChange={onChange} />
          <button type='submit'>Submit</button>
        </div>
        <div>
          {files.map(({ file, src, id }, index) => (
            <div key={`thumb${index}`} className='thumbnail-wrapper'>
              <img className='thumbnail' src={src} alt='' />
              <div className='thumbnail-caption'>{file.name}</div>
            </div>
          ))}
        </div>
      </form>
    </div>
  )
}

export default App
```

This basic component just renders a bunch of thumbnails when they're loaded. I didn't go too crazy with the styles since i'll leave that all up to you to have fun with :)

But if you want to use the basic styles here they are:

> src/App.css

```css
.thumbnail-wrapper {
  display: flex;
  align-items: center;
  padding: 6px 4px;
}

.thumbnail {
  flex-basis: 100px;
  height: 100%;
  max-width: 50px;
  max-height: 50px;
  object-fit: cover;
}

.thumbnail-caption {
  flex-grow: 1;
  font-size: 14px;
  color: #2b8fba;
  margin-bottom: 5px;
  padding: 0 12px;
}
```

What happens when all of the files are done uploading? Well, nothing yet really. But we can at least display something to the user to have them know that its done:

> src/App.js

```jsx
{
  status === 'FILES_UPLOADED' && (
    <div className='success-container'>
      <div>
        <h2>Congratulations!</h2>
        <small>You uploaded your files. Get some rest.</small>
      </div>
    </div>
  )
}
```

> src/App.css

```css
.success-container {
  position: absolute;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
}

.success-container h2,
small {
  color: green;
  text-align: center;
}
```

This time, _status_ is being used here. See, it's pretty useful, isn't it? You can make some pretty amazing complex looking UI with the other _status_ values as well when combined with _state.pending_ and others. Send me an email with a couple of screenshots if you did something amazin with this tutorial!

Final output:

> src/App.js

```jsx
import React from 'react'
import useFileHandlers from './useFileHandlers'
import './App.css'

const Input = (props) => (
  <input
    type='file'
    accept='image/*'
    name='img-loader-input'
    multiple
    {...props}
  />
)

const App = () => {
  const {
    files,
    pending,
    next,
    uploading,
    uploaded,
    status,
    onSubmit,
    onChange,
  } = useFileHandlers()

  return (
    <div className='container'>
      <form className='form' onSubmit={onSubmit}>
        {status === 'FILES_UPLOADED' && (
          <div className='success-container'>
            <div>
              <h2>Congratulations!</h2>
              <small>You uploaded your files. Get some rest.</small>
            </div>
          </div>
        )}
        <div>
          <Input onChange={onChange} />
          <button type='submit'>Submit</button>
        </div>
        <div>
          {files.map(({ file, src, id }, index) => (
            <div
              style={{
                opacity: uploaded[id] ? 0.2 : 1,
              }}
              key={`thumb${index}`}
              className='thumbnail-wrapper'
            >
              <img className='thumbnail' src={src} alt='' />
              <div className='thumbnail-caption'>{file.name}</div>
            </div>
          ))}
        </div>
      </form>
    </div>
  )
}

export default App
```

> src/App.css

(Media queries for mobile devices included)

```css
.container {
  padding: 8px;
  width: 100%;
  box-sizing: border-box;
  overflow-x: hidden;
}

.form {
  position: relative;
  width: 100%;
  height: 100%;
}

.form input,
button {
  margin-bottom: 15px;
}

.form button {
  padding: 8px 17px;
  border: 0;
  color: #fff;
  background: #265265;
  cursor: pointer;
}

.form button:hover {
  background: #1e3d4b;
}

.thumbnail-wrapper {
  display: flex;
  align-items: center;
  padding: 6px 4px;
}

.thumbnail {
  flex-basis: 100px;
  height: 100%;
  max-width: 50px;
  max-height: 50px;
  object-fit: cover;
}

.thumbnail-caption {
  flex-grow: 1;
  font-size: 14px;
  color: #2b8fba;
  margin-bottom: 5px;
  padding: 0 12px;
}

.success-container {
  position: absolute;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
}

.success-container h2,
small {
  color: green;
  text-align: center;
}

@media screen and (max-width: 472px) {
  .container {
    padding: 6px;
  }

  .thumbnail-wrapper {
    padding: 6px 2px;
  }

  .thumbnail {
    flex-basis: 40px;
    width: 100%;
    height: 100%;
    max-height: 40px;
    max-width: 40px;
  }

  .thumbnail-caption {
    font-size: 12px;
  }
}
```

## Screenshots

I provided some screenshots of a _basic_ UX implementation using the code in this tutorial:

### _onChange_

![onChange](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/keeping-ui-completely-synchronized-when-uploading-files-in-react/onChange.JPG)

### _logUploadedFile()_

![logUploadedFile](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/keeping-ui-completely-synchronized-when-uploading-files-in-react/console.JPG)

### state.pending

![state pending](https://jsmanifest.s3-us-west-1.amazonaws.com/posts/keeping-ui-completely-synchronized-when-uploading-files-in-react/pending.gif)

## Conclusion

That concludes the end of this post. I hope you liked it and stay tuned for more quality posts! :)