Upload de imagens no Front End com ReactJS e Context API

✅ Pré-requisitos

Sempre queremos entregar a melhor experiência para nossa audiência.

Para continuar com a leitura, certifique-se de cumprir os seguintes requisitos:

  • Ter lido sobre upload de imagens no S3 da AWS com Node.js e estar com o Back End do projeto rodando em sua máquina;
  • Conhecimento Básico em JavaScript e TypeScript;
  • Conhecimento Básico de ReactJS – estado, props e Context API;
  • Ter o Node.js e os gerenciadores de pacotes yarn ou npm instalados.

📋 Conteúdo

Essas são as etapas que vamos praticar hoje. Se você quiser, navegue pelo conteúdo. Olha só o que vai rolar nesse tutorial:

🔰 Introdução

Vamos implementar o sistema de upload de imagens no Front End com ReactJS utilizando o gerenciamento de estados com Context API.

Vamos refatorar o código que o Diego Fernandes fez nesse vídeo:

Na época foi utilizado React com Classes, passando funções e estados via props.

Desde então, nesses dois anos que passaram o o React evoluiu muito:

Vamos refatorar usando essas funcionalidades mais modernas e amplamente utilizadas no mercado.

👨‍💻 Bora Codar

Vou adicionar os códigos e explicar passo a passo:

Passo 1 - Criando o projeto e estruturando as pastas

Crie o projeto com React usando create-react-app com TypeScript:

npx create-react-app upload-frontend-react-hooks --template typescript

Organize a estrutura de pastas, crie uma pasta src e dentro dela as pastas components, context, services e styles:

Estrutura de pastas do projeto

Passo 2 - Instalando as dependências do projeto

Seu arquivo package.json deve ter essas dependências, portanto, copie e cole as que estiverem faltando  no seu projeto e execute yarn install para instalar.

{
  "name": "upload-frontend-react-hooks",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "@types/jest": "^24.0.0",
    "@types/lodash": "^4.14.161",
    "@types/node": "^12.0.0",
    "@types/react": "^16.9.0",
    "@types/react-dom": "^16.9.0",
    "@types/react-dropzone": "^5.1.0",
    "@types/styled-components": "^5.1.3",
    "@types/uuid": "^8.3.0",
    "axios": "^0.20.0",
    "filesize": "^6.1.0",
    "react": "^16.13.1",
    "react-circular-progressbar": "^2.0.3",
    "react-dom": "^16.13.1",
    "react-dropzone": "^11.1.0",
    "react-icons": "^3.11.0",
    "react-scripts": "3.4.3",
    "styled-components": "^5.2.0",
    "typescript": "~3.7.2",
    "uuid": "^8.3.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
package.json

Dependências:

As dependências que tem o prefixo: @types/ são as tipagens de cada biblioteca.

  • axios = para fazer requisições HTTP ao servidor;
  • filesize = mostrar de forma amigável o tamanho da imagem (2 MB, 4 GB, etc);
  • react-circular-progressbar = faz animação do progresso de envio da imagem;
  • react-dropzone = fica ouvindo o envio de imagens no front end;
  • react-icons = ícones da aplicação;
  • styled-components = auxilia na criação e estilização de componentes;
  • uuid = gerador de IDs no formato UUID.

Essas são as bibliotecas externas necessárias para criar essa aplicação.

Para executar o projeto, na raiz do projeto:

yarn start
// ou
npm start

Passo 3 - Estilizando a aplicação

Na pasta styles, crie o arquivo global.ts. Como o nome sugere, nesse arquivo terá toda estilização global do App:

01: import { createGlobalStyle } from "styled-components";
02: 
03: import "react-circular-progressbar/dist/styles.css";
04: 
05: export default createGlobalStyle`
06:   * {
07:     margin: 0;
08:     padding: 0;
09:     outline: 0;
10:     box-sizing: border-box;
11:   }
12: 
13:   body {
14:     font-family: Arial, Helvetica, sans-serif;
15:     font-size: 14px;
16:     background: #7159c1;
17:     text-rendering: optimizeLegibility;
18:     -webkit-font-smoothing: antialiased;
19:   }
20: 
21:   html, body, #root {
22:     height: 100%;
23:   }
24: `;

01 - importa a função createGlobalStyle da lib styled-components

03 - a lib react-circular-progressbar pede para fazer a importação da estilização dos componentes;

05 a 25 - exporta a função que faz a estilização global de toda a aplicação. Nesse arquivo deve conter toda a configuração geral do CSS que será compartilhado em todo o App;

Esse componente será importado no arquivo App.tsx, que veremos mais para frente.

Passo 4 - Criando os estilos  o componente App.tsx

Crie o arquivo src/styles.ts que irá estilizar o componente App.tsx:

01: import styled from "styled-components";
02: 
03: export const Container = styled.div`
04:   height: 100%;
05:   display: flex;
06:   justify-content: center;
07:   align-items: center;
08: `;
09: 
10: export const Content = styled.div`
11:   width: 100%;
12:   max-width: 400px;
13:   margin: 30px;
14:   background: #fff;
15:   border-radius: 4px;
16:   padding: 20px;
17: `;
18:

01 - importa o styled-components;

03 a 08 - cria uma div com nome Container e sua estilização;

10 a 17 - cria uma div com nome Content que estiliza a área onde irá ficar o conteúdo da aplicação;

Passo 5 - Criando o componente App.tsx

Substitua o conteúdo do arquivo App.tsx por esse:

01: import React from "react";
02: 
03: import GlobalStyle from "./styles/global";
04: import { Container, Content } from "./styles";
05: 
06: import Upload from "./components/Upload";
07: import FileList from "./components/FileList";
08: 
09: import { FileProvider } from "./context/files";
10: 
11: const App: React.FC = () => (
12:   <FileProvider>
13:     <Container>
14:       <Content>
15:         <Upload />
16:         <FileList />
17:       </Content>
18:       <GlobalStyle />
19:     </Container>
20:   </FileProvider>
21: );
22: 
23: export default App;
24:

01 - Todo arquivo React que utiliza sintaxe (jsx ou tsx)<ComponentX /> deve conter a importação do React;

03 - Importa a função createGlobalStyle e nomeamos como GlobalStyle do arquivo global.ts;

04 - Importa os componentes de estilização Container e Content do arquivo styles.ts;

06 e 07 - Importa os dois componentes da aplicação: Upload, que tem o dropzone onde vamos enviar os arquivos, e FileList, que é a listagem dos arquivos. Esses arquivos serão criados ao longo do tutorial;

09 - Importa FileProvider, que é o contexto que armazena o estado de cada imagem que será enviada na aplicação e controla as ações sobre esse estado;

11 a 23 - Cria um componente funcional App passando o FileProvider como componente raiz da aplicação, com isso o contexto será compartilhado entre os componentes filhos, netos, bisnetos... ou seja, tudo que estiver abaixo desse arquivo. Não precisamos ficar passando props, evitando assim o Prop Drilling;

13 - Adiciona o componente estilizado Container que estiliza a raiz da aplicação;

14 -  Adiciona o componente estilizado Content que recebe como conteúdo os dois componentes Upload e FileList;

23 - Exporta o App que será utilizando no arquivo index.ts, o qual irá renderizar o App.tsx com seus respectivos filhos no Front End.

Passo 6 - Criando o serviço de requisições HTTP com Axios

Para consumir a API do back end iremos utilizar o Axios.

Na pasta services vamos criar o arquivo api.ts:

import axios from "axios";

const api = axios.create({
  baseURL: "http://localhost:3000",
});

export default api;

O código é bem simples, lendo esse post você tem um overview sobre o Axios.

Passo 7 - Criando o componente Upload

Vamos criar a parte visual do upload de arquivos.

Crie a pasta Upload e o arquivo styles.ts: components/Upload/styles.ts

01: import styled, { css } from "styled-components";
02: 
03: const dragActive = css`
04:   border-color: #78e5d5;
05: `;
06: 
07: const dragReject = css`
08:   border-color: #e57878;
09: `;
10: 
11: type IDropContainer = {
12:   isDragActive?: boolean;
13:   isDragReject?: boolean;
14: };
15: 
16: export const DropContainer = styled.div<IDropContainer>`
17:   border: 1px dashed #ddd;
18:   border-radius: 4px;
19:   cursor: pointer;
20: 
21:   transition: height 0.2s ease;
22: 
23:   ${(props: any) => props.isDragActive && dragActive};
24:   ${(props: any) => props.isDragReject && dragReject};
25: `;
26: 
27: const messageColors = {
28:   default: "#999",
29:   error: "#e57878",
30:   success: "#78e5d5",
31: };
32: 
33: interface ITypeMessageColor {
34:   type?: "default" | "error" | "success";
35: }
36: 
37: export const UploadMessage = styled.p<ITypeMessageColor>`
38:   display: flex;
39:   color: ${(props) => messageColors[props.type || "default"]};
40:   justify-content: center;
41:   align-items: center;
42:   padding: 15px 0;
43: `;

Linha: 33 - Criamos a interface ITypeMessageColor, que tem a propriedade opcional type, com os tipos possíveis: default, error ou success, e aplicamos na linha 37, para a IDE e o código entenderem o props.type.

Nosso foco está na lógica e, para ganhar tempo, não vamos explicar a estilização aqui, caso você queira entender cada propriedade CSS, veja o vídeo dessa implementação.

Passo 8 - Criando a lógica do Upload

Na pasta Upload crie o arquivo index.tsx: components/Upload/index.tsx

01: import React, { useCallback } from "react";
02: import { useDropzone } from "react-dropzone";
03: import { useFiles } from "../../context/files";
04: 
05: import { DropContainer, UploadMessage } from "./styles";
06: 
07: function Upload() {
08:   const { handleUpload } = useFiles();
09: 
10:   const onDrop = useCallback(
11:     (files) => {
12:       handleUpload(files);
13:     },
14:     [handleUpload]
15:   );
16: 
17:   const {
18:     getRootProps,
19:     getInputProps,
20:     isDragActive,
21:     isDragReject,
22:   } = useDropzone({
23:     accept: ["image/jpeg", "image/pjpeg", "image/png", "image/gif"],
24:     onDrop,
25:   });
26: 
27:   const renderDragMessage = useCallback(() => {
28:     if (!isDragActive) {
29:       return <UploadMessage>Arraste imagens aqui...</UploadMessage>;
30:     }
31: 
32:     if (isDragReject) {
33:       return (
34:         <UploadMessage type="error">
35:           Tipo de arquivo não suportado
36:         </UploadMessage>
37:       );
38:     }
39: 
40:     return <UploadMessage type="success">Solte as imagens aqui</UploadMessage>;
41:   }, [isDragActive, isDragReject]);
42: 
43:   return (
44:     <DropContainer {...getRootProps()}>
45:       <input {...getInputProps()} />
46:       {renderDragMessage()}
47:     </DropContainer>
48:   );
49: }
50: 
51: export default Upload;

02 - Importa o hook useDropzone que lida com dropzone de arquivos;

03 - Importa o custom hook useFiles do contexto que iremos criar logo mais;

05 - Importa a estilização do componente, que criamos no passo anterior;

07 - Cria a função Upload;

08 - Disponibiliza a função handleUpload que vem do contexto do useFiles();

10 a 15 - Cria a função onDrop que recebe os arquivos (files) que são enviados no front end, invoca a função handleUpload para lidar com o upload do arquivo que está implementado no contexto no arquivo files.tsx e passa files como argumento;

17 a 25 - O hook useDropzone recebe um objeto de configuração aceitando a propriedade accept com um array de tipos de arquivos que podem ser enviados pelo o usuário e serão aceitos. Recebe também a função onDrop (que criamos nas linhas 10 a 15) como segundo parâmetro. Ela devolve, via desestruturação, as funções getRootProps e getInputProps, e as duas variáveis booleanas: isDragActive, que indica se está sendo enviado algum arquivo no dropzone, e isDragReject, que muda para true se o arquivo "dropado" é aceito ou false para rejeitado. Conforme a configuração da propriedade Accept;

27 a 41 - Cria a função renderDragMessage que implementa a lógica de como será exibido o dropzone. A primeira condição verifica se o isDragActive não tem arquivos arrastados no componente, retorna o componente UploadMessage que é uma tag <p> do HTML estilizado com sua respectiva mensagem;

Na linha 32, se o arquivo foi rejeitado, é enviado a prop error e o texto será alterado. Por fim, na linha 40 retorna o texto de sucesso, pois o isDragActive estará true e isDragReject estará false, ou seja, o arquivo pode ser solto no dropzone. Essa função está envolvida em um useCallback e recebe as variáveis isDragActive e isDragReject como dependência;

43 a 49: retorna o componente estilizado DropContainer (div) que recebe as propriedades do dropzone. Nessa div está o input do HTML com as propriedades do dropzone e, por fim, a função renderDragMessage é invocada, renderizando o texto de acordo com a lógica que explicamos na linha 27 a 41 acima;

51: Exporta o componente Upload para ser usado no App.tsx.

Componente Upload - Dropzone

Passo 9 - Criando o componente FileList

FileList é o componente de listagem das imagens que são enviadas.

Na pasta components crie a pasta FileList com os arquivos index.tsx e styles.ts

No arquivo styles.ts adicione o código da estilização:

import styled from "styled-components";

export const Container = styled.ul`
  margin-top: 20px;

  li {
    display: flex;
    justify-content: space-between;
    align-items: center;
    color: #444;

    & + li {
      margin-top: 15px;
    }
  }
`;

export const FileInfo = styled.div`
  display: flex;
  align-items: center;

  div {
    display: flex;
    flex-direction: column;

    span {
      font-size: 12px;
      color: #999;
      margin-top: 5px;

      button {
        border: 0;
        background: transparent;
        color: #e57878;
        margin-left: 5px;
        cursor: pointer;
      }
    }
  }
`;

interface PreviewProps {
  src?: string;
}

export const Preview = styled.div<PreviewProps>`
  width: 36px;
  height: 36px;
  border-radius: 5px;
  background-image: url(${(props) => props.src});
  background-repeat: no-repeat;
  background-size: cover;
  background-position: 50% 50%;
  margin-right: 10px;
`;

O componente estilizado Container é uma li do HTML com sua respectiva estilização.

FileInfo contém toda estilização das tags HTML que vamos utilizar no index.tsx.

Preview é uma div estilizada que mostra uma pequena imagem do arquivo enviado.

Passo 10 - Criando a lógica do FileList

No arquivo index.tsx, que foi criado no passo anterior, adicione esse código:

components/FileList/index.tsx

01: import React from "react";
02: import { CircularProgressbar } from "react-circular-progressbar";
03: import { MdCheckCircle, MdError, MdLink, MdMoodBad } from "react-icons/md";
04: import { useFiles } from "../../context/files";
05: import { IFile } from "../../context/files";
06: 
07: import { Container, FileInfo, Preview } from "./styles";
08: 
09: const FileList = () => {
10:   const { uploadedFiles: files, deleteFile } = useFiles();
11: 
12:   if (!files.length)
13:     return (
14:       <span>
15:         <MdMoodBad
16:           style={{ marginLeft: "45%", marginTop: 10 }}
17:           size={24}
18:           color="#d5d2d2"
19:         />
20:       </span>
21:     );
22: 
23:   return (
24:     <Container>
25:       {files.map((uploadedFile: IFile) => (
26:         <li key={uploadedFile.id}>
27:           <FileInfo>
28:             <Preview src={uploadedFile.preview} />
29:             <div>
30:               <strong>{uploadedFile.name}</strong>
31:               <span>
32:                 {uploadedFile.readableSize}{" "}
33:                 {!!uploadedFile.url && (
34:                   <button onClick={(e) => deleteFile(uploadedFile.id)}>
35:                     Excluir
36:                   </button>
37:                 )}
38:               </span>
39:             </div>
40:           </FileInfo>
41: 
42:           <div>
43:             {!uploadedFile.uploaded && !uploadedFile.error && (
44:               <CircularProgressbar
45:                 styles={{
46:                   root: { width: 24 },
47:                   path: { stroke: "#7159c1" },
48:                 }}
49:                 strokeWidth={10}
50:                 text={String(uploadedFile.progress)}
51:                 value={uploadedFile.progress || 0}
52:               />
53:             )}
54: 
55:             {uploadedFile.url && (
56:               <a
57:                 href={uploadedFile.url}
58:                 target="_blank"
59:                 rel="noopener noreferrer"
60:               >
61:                 <MdLink style={{ marginRight: 8 }} size={24} color="#222" />
62:               </a>
63:             )}
64: 
65:             {uploadedFile.uploaded && (
66:               <MdCheckCircle size={24} color="#78e5d5" />
67:             )}
68:             {uploadedFile.error && <MdError size={24} color="#e57878" />}
69:           </div>
70:         </li>
71:       ))}
72:     </Container>
73:   );
74: };
75: 
76: export default FileList;

02 - Importa o componente CircularProgress que contém toda estilização e lógica para exibir o loading de progresso;

03 - Importa os ícones de status e ação do usuário;

04 -Importa o useFiles que é o contexto que contém o estado e funções dos arquivos enviados;

05 - Importa a interface IFile que é a representação do objeto File (que será criado logo mais adiante);

07 - Importa os componentes estilizados que foram criados no passo anterior;

09 - Cria o componente funcional FileList ;

10 - Acessa a variável uploadedFiles renomeando para files e acessa a função deleteFile, ambas do contexto useFiles;

12 a 21 - É feito uma verificação condicional que renderiza um ícone representando que a lista está vazia;

23 a 73 - Retorna um container que possui seus componentes filhos. É feito um loop usando o map do array files para criar um componente para cada arquivo enviado, mostrando seu preview (imagem), nome do arquivo, tamanho em MB e a URL para acessar o arquivo;

34 - O componente tem um botão que, quando clicado, chama a função deleteFile;

43 a 53 - Componente CircularProgressbar é exibido enquanto o arquivo não foi enviado (uploaded = false) ou se não ocorreu algum erro;

55 - URL do arquivo é exibida assim que ela recebida do servidor;

65 - Quando finaliza o upload, o loading de progresso para de aparecer e aparece um ícone representando o status de finalizado no lugar do loading;

76 - Exportamos o componente para ser utilizado no App.tsx

Repare que é um componente que não possui comportamento, ele apenas recebe o estado e funções que alteram o estado via hook do contexto. Isso é um plus no componente, pois sua responsabilidade é renderizar os componentes e invocar funções, e o contexto que lida com lógica da manipulação de estado e o comportamento das funções.

Componente da Listagem de arquivos enviados

Passo 11 - Criando o contexto que armazena o estado dos arquivos enviados

Na pasta context crie o arquivo files.tsx: src/context/files.tsx. Copie e cole o código abaixo.

Nesse código estamos criando o hook de contexto usando a Context API do React, ela será responsável por pegar os dados do back end usando o serviço API que criamos no passo 5. Armazenar esses valores no estado uploadedFiles e fazer a manipulação no estado conforme as ações do usuário de inserir nova imagem via upload ou deletar um arquivo.

Leia o código abaixo e, em seguida, os comentários de cada trecho relevante.

001: import React, {
002:   createContext,
003:   useState,
004:   useEffect,
005:   useCallback,
006:   useContext,
007: } from "react";
008: import { v4 as uuidv4 } from "uuid";
009: import filesize from "filesize";
010: 
011: import api from "../services/api";
012: 
013: export interface IPost {
014:   _id: string;
015:   name: string;
016:   size: number;
017:   key: string;
018:   url: string;
019: }
020: 
021: export interface IFile {
022:   id: string;
023:   name: string;
024:   readableSize: string;
025:   uploaded?: boolean;
026:   preview: string;
027:   file: File | null;
028:   progress?: number;
029:   error?: boolean;
030:   url: string;
031: }
032: 
033: interface IFileContextData {
034:   uploadedFiles: IFile[];
035:   deleteFile(id: string): void;
036:   handleUpload(file: any): void;
037: }
038: 
039: const FileContext = createContext<IFileContextData>({} as IFileContextData);
040: 
041: const FileProvider: React.FC = ({ children }) => {
042:   const [uploadedFiles, setUploadedFiles] = useState<IFile[]>([]);
043: 
044:   useEffect(() => {
045:     api.get<IPost[]>("posts").then((response) => {
046:       const postFormatted: IFile[] = response.data.map((post) => {
047:         return {
048:           ...post,
049:           id: post._id,
050:           preview: post.url,
051:           readableSize: filesize(post.size),
052:           file: null,
053:           error: false,
054:           uploaded: true,
055:         };
056:       });
057: 
058:       setUploadedFiles(postFormatted);
059:     });
060:   }, []);
061: 
062:   useEffect(() => {
063:     return () => {
064:       uploadedFiles.forEach((file) => URL.revokeObjectURL(file.preview));
065:     };
066:   });
067: 
068:   const updateFile = useCallback((id, data) => {
069:     setUploadedFiles((state) =>
070:       state.map((file) => (file.id === id ? { ...file, ...data } : file))
071:     );
072:   }, []);
073: 
074:   const processUpload = useCallback(
075:     (uploadedFile: IFile) => {
076:       const data = new FormData();
077:       if (uploadedFile.file) {
078:         data.append("file", uploadedFile.file, uploadedFile.name);
079:       }
080: 
081:       api
082:         .post("posts", data, {
083:           onUploadProgress: (progressEvent) => {
084:             let progress: number = Math.round(
085:               (progressEvent.loaded * 100) / progressEvent.total
086:             );
087: 
088:             console.log(
089:               `A imagem ${uploadedFile.name} est√° ${progress}% carregada... `
090:             );
091: 
092:             updateFile(uploadedFile.id, { progress });
093:           },
094:         })
095:         .then((response) => {
096:           console.log(
097:             `A imagem ${uploadedFile.name} j√° foi enviada para o servidor!`
098:           );
099: 
100:           updateFile(uploadedFile.id, {
101:             uploaded: true,
102:             id: response.data._id,
103:             url: response.data.url,
104:           });
105:         })
106:         .catch((err) => {
107:           console.error(
108:             `Houve um problema para fazer upload da imagem ${uploadedFile.name} no servidor AWS`
109:           );
110:           console.log(err);
111: 
112:           updateFile(uploadedFile.id, {
113:             error: true,
114:           });
115:         });
116:     },
117:     [updateFile]
118:   );
119: 
120:   const handleUpload = useCallback(
121:     (files: File[]) => {
122:       const newUploadedFiles: IFile[] = files.map((file: File) => ({
123:         file,
124:         id: uuidv4(),
125:         name: file.name,
126:         readableSize: filesize(file.size),
127:         preview: URL.createObjectURL(file),
128:         progress: 0,
129:         uploaded: false,
130:         error: false,
131:         url: "",
132:       }));
133: 
134:       // concat é mais performático que ...spread
135:       // https://www.malgol.com/how-to-merge-two-arrays-in-javascript/
136:       setUploadedFiles((state) => state.concat(newUploadedFiles));
137:       newUploadedFiles.forEach(processUpload);
138:     },
139:     [processUpload]
140:   );
141: 
142:   const deleteFile = useCallback((id: string) => {
143:     api.delete(`posts/${id}`);
144:     setUploadedFiles((state) => state.filter((file) => file.id !== id));
145:   }, []);
146: 
147:   return (
148:     <FileContext.Provider value={{ uploadedFiles, deleteFile, handleUpload }}>
149:       {children}
150:     </FileContext.Provider>
151:   );
152: };
153: 
154: function useFiles(): IFileContextData {
155:   const context = useContext(FileContext);
156: 
157:   if (!context) {
158:     throw new Error("useFiles must be used within FileProvider");
159:   }
160: 
161:   return context;
162: }
163: 
164: export { FileProvider, useFiles };

001 a 011 - Importação dos códigos necessários

013 a 019 - Cria e exporta a interface IPost que define o formado do objeto que estará vindo da API;

021 a 031 - Cria e exporta a interface IFile que define o objeto e suas propriedades do arquivo;

033 a 037 - Cria a interface IFileContextData que define o formato do objeto do contexto;

039 - Cria o contexto FileContext utilizando a função createContext seguindo o contrato definido na interface IFileContextData;

041 a 152 - Cria o componente funcional FileProvider que recebe um children como parâmetro que são todos os componentes abaixo do FileProvider que definimos no App.tsx;

042 - O estado uploadFiles do tipo IFile[] que será manipulado pelo hook que estamos criando e será fornecido ao contexto no return;

044 a 060 - Cria um useEffect para buscar os dados do back end utilizando o axios, o valor é formatado. São adicionados alguns valores iniciais seguindo o contrato da interface IFile, uma vez que os dados vem do tipo IPost.  No final é adicionado na variável de estado uploadedFiles na linha 058. Essa função é executada assim que o componente monta em tela;

062 a 066 - Utiliza o useEffect para invocar a função de retorno assim que esse componente sair da tela, com isso será liberado memória do navegador, uma vez que foi utilizado  URL.createObjectURL(file) na linha 127 para criar uma string de URL, baseado no file, para o browser conseguir abrir o arquivo. A função é executada assim que a aplicação é fechada;

068 a 72 - Cria a função updateFile que recebe um id e data como parâmetro. Essa função será executada toda vez que o estado do UploadedFiles for alterado;

074 a 118 - Cria a função processUpload que recebe o uploadedFile como parâmetro. Essa função é responsável por processar cada arquivo que está sendo enviado pelo usuário, chamando a API e fazendo um POST com o arquivo e o nome do arquivo;

FormData representa o tipo de configuração multipart/form-data que é utilizado para enviar arquivos. É feito um append passando a chave file com o arquivo e o seu nome;

Utiliza a função onUploadProgress para ouvir o progresso do envio da imagem para o servidor, explicamos detalhadamente esse processo neste post. Observe que a função updateFile é executada sempre que o progresso altera, passando como data a propriedade alterada;

120 a 140 - Cria a função handleUpload que lida com os arquivos que foram recebidos no dropzone do componente Upload. A constante newUploadedFiles recebe um novo array gerado no final da execução do files.map com o próprio file enviado pelo usuário, id que é gerado temporariamente pela lib uuidv4 (para facilitar com o processo da lógica e cria um preview com o file enviado para gerar uma URL para abrir a imagem no browser), são inseridas outras propriedades que são necessárias para satisfazer o contrato da interface IFile. Para cada newUploadedFiles executa a função processUpload que enviará o arquivo (file) para o servidor, conforme explicamos anteriormente nas linhas 074 a 118;

142 a 145 - Cria a função delete que recebe o id do post que será deletado, utilizamos a estratégica de Optimistic UI. Mesmo que ocorra alguma falha na deleção do arquivo no back end, já alteramos a tela do usuário removendo o arquivo usando o filter e removendo do array do item que foi selecionado para excluir;

147 a 151 - Retorna o componente FileContext e fornece no Provider os valores. Dentro do value um objeto global que é acessado por todo o contexto da aplicação onde o provider fornece. UploadedFiles, deleteFiles e handleUpload ficam disponíveis;

154 a 162 - Cria um hook useFiles que retorna o contexto FileContext, dessa forma, quando o hook useFiles for chamado, irá retornar os valores do Provider. Essa função só pode ser utilizada dentro do contexto de FileProvider;

164 - Exporta o FileProvider que será usado no App.tsx e useFiles que será utilizado em qualquer lugar que desejarmos ter acesso a uploadedFiles, deleteFile e handleUpload.

Ufa, chegamos ao fim da aplicação! Agora vamos executar o projeto.

Passo 12 - Executando o projeto

Para inicializar o projeto, execute o comando abaixo na raiz do projeto:

yarn start

Vamos ter o seguinte resultado:

Conforme você for enviado arquivos para o servidor, terá esse fluxo:

Conclusão

Utilizamos alguns conceitos bem legais do React Hooks — useState, useEffect, useCallback; Context API, busca de dados com API usando o método GET e envio de arquivos com método POST. Alteração de estado de maneira assíncrona com React, com melhoria de performance.

Podemos processar arquivos por vez e obter o progresso de cada um e mostramos na tela.

Tivemos um exemplo prático de refatoração de código, onde alteramos o código sem mudar o comportamento. Modernizando o código de React com classes para React Hooks.

Conseguimos conectar o Front End em uma aplicação Back End que construimos nesse post. Criando um projeto Full Stack.

🛡️ Desafio

Agora vou deixar um desafio: Recriar esse App (Back End e Front End) usando Next.js. Hospedar essa aplicação na Vercel. E aí, bora codar?

Gif de um foguete sendo lançado para o espaço.

E aí, o que achou do post?

Espero que tenha curtido! 💜

O aprendizado é contínuo e sempre haverá um próximo nível! 🚀