在 React 中上传文件并保持 UI 完全同步

2025-05-25

在 React 中上传文件并保持 UI 完全同步

在Medium上找到我

构建文件上传组件是一项非常重要的技能,因为它允许用户在本地环境之外选择和发送文件。

话虽如此,本文重点介绍 JavaScript 中的原生文件API。如果您想进一步了解文件 API 的工作原理,请点击此处

一开始,构建文件上传组件可能比较难掌握,尤其是当你想自定义它的外观和感觉的时候。(我们可以在以后的教程中讨论如何自定义文件输入组件的设计)。但一旦你很好地理解了这些概念,其实并没有那么难

我的意思是,你可以创建一个文件输入元素,传入 onChange 事件,然后就完事了。但是,你会通过每次都向用户展示流程的当前状态来照顾用户吗?还是你只是让他们坐在那里,希望他们看到流程的结束,中间没有任何视觉更新?

如果用户的网络断线了怎么办?如果服务器没有任何响应怎么办?如果14个文件里第8个文件对他们来说太大怎么办?如果用户等待上传过程10分钟才完成,想看看进度如何?或者哪些文件已经上传完毕怎么办?

如果希望用户体验保持一致,就必须让用户持续了解后台运行情况。专业且一致的用户界面有助于从技术角度建立用户对你应用的信任。如果你计划开发一款用户注册并付费使用你提供的服务的应用,那么他们必须信任你提供的技术,并且相信你的技术比其他任何人都更优秀。你正在使用 React 进行开发,你拥有超越一切的力量!

但我该从哪里开始呢?

别担心!这篇文章将教如何创建一个包含文件上传组件的用户界面,帮助用户选择文件并将其发送到某个位置,同时允许界面从实例化到最终状态更新。创建组件是一回事,但在整个过程中让 UI 与状态更新同步又是另一回事。

让我们开始吧!

在本教程中,我们将使用create-react-app快速生成一个 React 项目。

继续使用以下命令创建一个项目。在本教程中,我将其命名为file-upload-with-ux。

npx create-react-app file-upload-with-ux
Enter fullscreen mode Exit fullscreen mode

完成后进入目录:

cd file-upload-with-ux
Enter fullscreen mode Exit fullscreen mode

我们要做的第一件事是打开App.js并用我们自己的实现替换默认代码:

src/App.js

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
Enter fullscreen mode Exit fullscreen mode

在这里,我们定义了一个表单元素并传递了一个onSubmit处理程序,以便我们可以访问用户提交后选择的所有文件。

在表单内部,我们定义了文件输入组件,允许用户选择任意文件。输入组件接收一个onChange处理函数,因此我们也将其传入。onChange 处理函数可以通过访问第一个参数中的e.target.files来接收文件。

我在里面应用了一些基本样式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;
}
Enter fullscreen mode Exit fullscreen mode

至此,我们已经设置好了基本组件,并设置好了相应的处理程序。现在,我们将创建一个自定义的 React Hook,以便将所有繁琐的状态逻辑都放在里面,远离 UI 组件。

我要这样称呼它useFileHandlers.js

src/useFileHandlers.js

import React from 'react'

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

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

export default useFileHandlers
Enter fullscreen mode Exit fullscreen mode

整篇文章最重要的部分可能是上面显示的initialState。它使得 UI 能够捕捉文件上传过程的每个时刻。

files是用户最初通过从文件输入中选择文件来加载文件数组的地方。

pending将用于让 UI 知道当前正在处理哪个文件以及还剩下多少个文件。

当代码检测到已准备好执行此操作时,next将被分配待处理数组中的下一个项目。

上传将用于让代码知道文件仍在上传。

uploaded将是文件上传完成后我们插入到的对象。

最后,提供状态作为额外的便利,主要是为了用户界面充分利用其优势。

我们将使用来自 react 的useReducer hook api,因为它非常适合我们的用途。

但首先,让我们在useFileHandlers钩子上方定义一些常量,以确保稍后应用状态更新时不会输入错误:

src/useFileHandlers.js

const LOADED = 'LOADED'
const INIT = 'INIT'
const PENDING = 'PENDING'
const FILES_UPLOADED = 'FILES_UPLOADED'
const UPLOAD_ERROR = 'UPLOAD_ERROR'
Enter fullscreen mode Exit fullscreen mode

这些将进入作为useReducer第一个参数传入的reducer

现在定义减速器:

src/useFileHandlers.js

const reducer = (state, action) => {
  switch (action.type) {
    default:
      return state
  }
}
Enter fullscreen mode Exit fullscreen mode

我们现在可能不应该忘记从 react导入useReducer ,是吗?

src/useFileHandlers.js

import { useReducer } from 'react'
Enter fullscreen mode Exit fullscreen mode

现在将 state/dispatch api 定义到钩子中:

src/useFileHandlers.js

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

  return {}
}

export default useFileHandlers
Enter fullscreen mode Exit fullscreen mode

现在我们将回到之前设置的onChange实现并进一步增强它。

在执行此操作之前,让我们在 Reducer 中添加一个新的 Switch Case:

src/useFileHandlers.js

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

这将允许 onChange 处理程序在调用时立即将文件传递到状态中:

src/useFileHandlers.js

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 })
  }
}
Enter fullscreen mode Exit fullscreen mode

这里要注意的是,当我们从事件对象中检索e.target.files时,它不是一个数组——它是一个FileList

我们将其转换为数组的原因是,UI 组件可以映射它们并显示文件大小和文件类型等有用信息。否则,组件在尝试映射 *FileList* 时会导致应用崩溃。

到目前为止,这是我们自定义钩子的完整实现:

src/useFileHandlers.js

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
Enter fullscreen mode Exit fullscreen mode

现在我们将重点介绍另一个处理程序—— onSubmit。它在用户提交表单时调用(这是显而易见的)。在onSubmit处理程序内部,我们用useCallback将其包装起来,以便它始终获取最新的状态值。

src/useFileHandlers.js

import { useCallback, useReducer } from 'react'
Enter fullscreen mode Exit fullscreen mode

src/useFileHandlers.js

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],
)
Enter fullscreen mode Exit fullscreen mode

此 onSubmit 处理程序在 onChange之后调用,因此它可以从刚刚由onChange设置的state.files中提取文件,以实例化上传过程。

为了实例化上传过程,我们需要另一个 switch case:

src/useFileHandlers.js

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
  }
}
Enter fullscreen mode Exit fullscreen mode

好的,现在发生的情况如下:

  1. 它将state.uploading改为true。当你将 state.uploading 改为true时,你就可以开始对 UI 组件进行破坏,并向用户显示任何你想要的内容,只要他们明白你试图向他们传达文件正在上传的信息。

  2. 它使用用户选择的所有文件来初始化state.pending。你也可以用它来对 UI 组件进行一些改动。状态的这一部分有很多使用方法。不过现在我先跳过这部分,因为我想先和你一起完成整个教程 :)

  3. 它将状态的便捷部分status设置为"INIT"。您也可以在钩子或 UI 中的某个地方使用它来触发某些 " onStart " 逻辑,或者任何您想要的操作——因为在新的上传过程开始之前,它永远不会回到这个值。

现在我们将返回状态以及 onSubmit 和 onChange 处理程序,以便 UI 可以顺利访问它们:

src/useFileHandlers.js

return {
  ...state,
  onSubmit,
  onChange,
}
Enter fullscreen mode Exit fullscreen mode

src/useFileHandlers.js

接下来我们要处理的是useEffect部分。我们需要useEffect 来实现“运行到完成”的功能。

这些 useEffects 是本教程中非常重要的实现,因为它们在 UI 和自定义钩子之间创建了完美、一致的同步流——正如您稍后将看到的,它们无处不在。

src/useFileHandlers.js

import { useCallback, useEffect, useReducer } from 'react'
Enter fullscreen mode Exit fullscreen mode

我们将定义我们的第一个 useEffect,它将负责在检测到准备好时立即促进下一个文件的上传(只要state.pending中仍有项目):

src/useFileHandlers.js

// 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])
Enter fullscreen mode Exit fullscreen mode

它抓取state.pending数组中的下一个可用文件,并使用dispatch创建信号,将该文件作为下一个state.next对象发送:

src/useFileHandlers.js

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
  }
}
Enter fullscreen mode Exit fullscreen mode

为了方便起见,我们在此处添加了“待处理”状态。如何处理此上传过程完全由您自行决定!

下一个代码片段将显示我提供的实用函数,该函数仅用于帮助记录到控制台供您查看,仅供本教程使用。

src/useFileHandlers.js

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

我们接下来要应用的第二个 useEffect将负责上传刚刚在状态中设置的下一个文件:

src/useFileHandlers.js

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])
Enter fullscreen mode Exit fullscreen mode

在.then()处理程序中,我创建了一个新的变量prev ,并将下一个已完成上传的对象赋值给它。这只是为了提高可读性,因为我们不想在 switch case 中混淆,稍后我们会看到。

你可能注意到了,里面有一个useRef函数。是的,我承认,我这么做了。但我这么做的原因是,我们要在我提供的logUploadedFile工具函数中使用它并进行修改。

src/useFileHandlers.js

import { useCallback, useEffect, useReducer, useRef } from 'react'
Enter fullscreen mode Exit fullscreen mode

哦,如果您需要一些模拟函数来模拟代码片段中看到的“上传”承诺处理程序,您可以使用这个:

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

现在继续通过应用“file-uploaded”“set-upload-error”开关情况来更新您的reducer:

src/useFileHandlers.js

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
  }
}
Enter fullscreen mode Exit fullscreen mode

对于文件上传的情况,我们将next重置为null ,以便第一个 useEffect可以再次响应它。当它响应时,它会拉取state.pending队列中的下一个文件,并将其赋值给下一个state.next值。你已经可以看到它是如何成为一个自运行的进程——一个运行到完成的实现!

无论如何,我们将刚刚上传的文件应用到state.uploaded对象,以便 UI 也能利用它。这在本教程中也是一个非常有用的功能,因为如果你正在渲染一堆缩略图,你可以在它们上传后动态地为每一行添加阴影!:) 截图在本文末尾。

第三个useEffect将负责通过向reducer发送文件上传信号来关闭上传过程:

src/useFileHandlers.js

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

将其添加到减速器中看起来是这样的:

src/useFileHandlers.js

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
  }
}
Enter fullscreen mode Exit fullscreen mode

自定义钩子终于完成了!太棒了!

以下是自定义钩子的最终代码:

src/useFileHandlers.js

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
Enter fullscreen mode Exit fullscreen mode

可是等等,还没完呢。我们还需要把这个逻辑应用到用户界面上。哎呀!

我们将导入useFileHandlers钩子并在组件中使用它。我们还将为每个文件创建 UI 映射,并将它们渲染为缩略图:

src/App.js

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
Enter fullscreen mode Exit fullscreen mode

这个基础组件在加载时只会渲染一些缩略图。样式方面我没有太过复杂,因为一切都留给大家自己去发挥吧 :)

但是如果你想使用这里的基本样式,它们是:

src/App.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;
}
Enter fullscreen mode Exit fullscreen mode

所有文件上传完成后会发生什么?嗯,实际上还没有。但我们至少可以向用户显示一些内容,让他们知道上传完成了:

src/App.js

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

src/App.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;
}
Enter fullscreen mode Exit fullscreen mode

这次用到了status 。瞧,它很实用,不是吗?你也可以用其他status值,结合state.pending和其他一些属性,制作出一些非常精美复杂的 UI。如果你用本教程做出了一些很棒的作品,请给我发邮件,并附上几张截图!

最终输出:

src/App.js

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
Enter fullscreen mode Exit fullscreen mode

src/App.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;
  }
}
Enter fullscreen mode Exit fullscreen mode

截图

我提供了使用本教程中的代码实现的基本UX的一些屏幕截图:

onChange

onChange

记录上传文件()

记录上传文件

状态待定

状态待定

结论

这篇文章到此结束。希望你喜欢,并敬请期待更多精彩文章!:)

文章来源:https://dev.to/jsmanifest/keeping-ui-completely-synchronized-when-uploading-files-in-react-dj3
PREV
JavaScript 中的命令设计模式
NEXT
Electron React 使用热重载在 Electron 中创建你的第一个 React 桌面应用程序