documentation/docs/advanced-tutorials/upload/multipart-upload.md
import { Refine } from "@refinedev/core";
import { AuthPage, RefineThemes, ThemedLayout, ErrorComponent, useNotificationProvider } from "@refinedev/antd";
import routerProvider, { NavigateToResource } from "@refinedev/react-router";
import { ConfigProvider } from "antd";
import { BrowserRouter, Routes, Route, Outlet } from "react-router";
import dataProvider from "@refinedev/simple-rest";
const API_URL = "https://api.fake-rest.refine.dev";
import {
useMany as CoreUseMany,
useShow as RefineCoreUseShow,
useOne as RefineCoreUseOne,
useApiUrl as RefineCoreUseApiUrl,
} from "@refinedev/core";
import {
List as RefineAntdList,
TextField as RefineAntdTextField,
useTable as RefineAntdUseTable,
EditButton as RefineAntdEditButton,
ShowButton as RefineAntdShowButton,
useForm as RefineAntdUseForm,
useSelect as RefineAntdUseSelect,
Create as RefineAntdCreate,
Edit as RefineAntdEdit,
Show as RefineAntdShow,
getValueFromEvent as RefineAntdGetValueFromEvent,
} from "@refinedev/antd";
import {
Table as AntdTable,
Space as AntdSpace,
Form as AntdForm,
Select as AntdSelect,
Input as AntdInput,
Typography as AntdTypography,
Upload as AntdUpload,
} from "antd";
const PostList: React.FC = () => {
const { result, tableProps } = RefineAntdUseTable<IPost>();
const categoryIds = result?.data?.map((item) => item.category.id) ?? [];
const { result: categoryResult, query: { isLoading } } = CoreUseMany<ICategory>({
resource: "categories",
ids: categoryIds,
queryOptions: {
enabled: categoryIds.length > 0,
},
});
return (
<RefineAntdList>
<AntdTable {...tableProps} rowKey="id">
<AntdTable.Column
dataIndex="id"
title="ID"
/>
<AntdTable.Column dataIndex="title" title="Title" />
<AntdTable.Column
dataIndex={["category", "id"]}
title="Category"
render={(value) => {
if (isLoading) {
return <RefineAntdTextField value="Loading..." />;
}
return (
<RefineAntdTextField
value={
result?.data.find((item) => item.id === value)
?.title
}
/>
);
}}
/>
<AntdTable.Column<IPost>
title="Actions"
dataIndex="actions"
render={(_, record) => (
<AntdSpace>
<RefineAntdEditButton
hideText
size="small"
recordItemId={record.id}
/>
<RefineAntdShowButton
hideText
size="small"
recordItemId={record.id}
/>
</AntdSpace>
)}
/>
</AntdTable>
</RefineAntdList>
);
};
const PostCreate: React.FC = () => {
const { formProps, saveButtonProps } = RefineAntdUseForm<IPost>();
const { selectProps: categorySelectProps } = RefineAntdUseSelect<ICategory>(
{
resource: "categories",
},
);
const apiUrl = RefineCoreUseApiUrl();
return (
<RefineAntdCreate saveButtonProps={saveButtonProps}>
<AntdForm {...formProps} layout="vertical">
<AntdForm.Item
label="Title"
name="title"
rules={[
{
required: true,
},
]}
>
<AntdInput />
</AntdForm.Item>
<AntdForm.Item label="Image">
<AntdForm.Item
name="image"
valuePropName="fileList"
getValueFromEvent={RefineAntdGetValueFromEvent}
noStyle
>
<AntdUpload.Dragger
name="file"
action={`${apiUrl}/media/upload`}
listType="picture"
maxCount={5}
multiple
>
<p className="ant-upload-text">
Drag & drop a file in this area
</p>
</AntdUpload.Dragger>
</AntdForm.Item>
</AntdForm.Item>
</AntdForm>
</RefineAntdCreate>
);
};
const PostEdit: React.FC = () => {
const { formProps, saveButtonProps, query } =
RefineAntdUseForm<IPost>();
const postData = query?.data?.data;
const { selectProps: categorySelectProps } = RefineAntdUseSelect<ICategory>({
resource: "categories",
defaultValue: postData?.category.id,
});
const apiUrl = RefineCoreUseApiUrl();
return (
<RefineAntdEdit saveButtonProps={saveButtonProps}>
<AntdForm {...formProps} layout="vertical">
<AntdForm.Item
label="Title"
name="title"
rules={[
{
required: true,
},
]}
>
<AntdInput />
</AntdForm.Item>
<AntdForm.Item label="Image">
<AntdForm.Item
name="image"
valuePropName="fileList"
getValueFromEvent={RefineAntdGetValueFromEvent}
noStyle
>
<AntdUpload.Dragger
name="file"
action={`${apiUrl}/media/upload`}
listType="picture"
maxCount={5}
multiple
>
<p className="ant-upload-text">
Drag & drop a file in this area
</p>
</AntdUpload.Dragger>
</AntdForm.Item>
</Form.Item>
</AntdForm>
</RefineAntdEdit>
);
};
const PostShow: React.FC = () => {
const { query, result: post } = RefineCoreUseShow<IPost>();
const { isLoading } = query;
const { result: category, isLoading: categoryIsLoading } =
RefineCoreUseOne<ICategory>({
resource: "categories",
id: post?.category?.id || "",
queryOptions: {
enabled: !!post,
},
});
return (
<RefineAntdShow isLoading={isLoading}>
<AntdTypography.Title level={5}>Id</AntdTypography.Title>
<AntdTypography.Text>{post?.id}</AntdTypography.Text>
<AntdTypography.Title level={5}>
AntdTypography.Title
</AntdTypography.Title>
<AntdTypography.Text>{post?.title}</AntdTypography.Text>
<AntdTypography.Title level={5}>Category</AntdTypography.Title>
<AntdTypography.Text>
{categoryIsLoading ? "Loading..." : category?.title}
</AntdTypography.Text>
<AntdTypography.Title level={5}>Content</AntdTypography.Title>
<AntdTypography.Text>{post?.content}</AntdTypography.Text>
</RefineAntdShow>
);
};
We will demonstrate how to perform a multipart upload with Refine.
Let's start with the creation form first.
Let's add the image field to the post creation form.
import {
// highlight-start
useApiUrl,
// highlight-end
} from "@refinedev/core";
import {
// highlight-start
getValueFromEvent,
// highlight-end
Create,
useForm,
} from "@refinedev/antd";
import {
// highlight-next-line
Upload,
Form,
Input,
} from "antd";
export const PostCreate: React.FC = () => {
const { formProps, saveButtonProps } = useForm<IPost>();
// highlight-next-line
const apiUrl = useApiUrl();
return (
<Create saveButtonProps={saveButtonProps}>
<Form {...formProps} layout="vertical">
<Form.Item
label="Title"
name="title"
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item label="Image">
<Form.Item
name="image"
valuePropName="fileList"
// highlight-next-line
getValueFromEvent={getValueFromEvent}
noStyle
>
// highlight-start
<Upload.Dragger
name="file"
action={`${apiUrl}/media/upload`}
listType="picture"
maxCount={5}
multiple
>
<p className="ant-upload-text">Drag & drop a file in this area</p>
</Upload.Dragger>
// highlight-end
</Form.Item>
</Form.Item>
</Form>
</Create>
);
};
interface IPost {
id: number;
title: string;
image: [
{
uid: string;
name: string;
url: string;
status: "error" | "success" | "done" | "uploading" | "removed";
},
];
}
:::tip
We can reach the API URL by using the useApiUrl hook.
:::
It will look like this.
setInitialRoutes(["/posts/create"]);
const App = () => {
return (
<BrowserRouter>
<ConfigProvider theme={RefineThemes.Blue}>
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider(API_URL)}
resources={[
{
name: "posts",
list: "/posts",
create: "/posts/create",
show: "/posts/show/:id",
edit: "/posts/edit/:id",
},
]}
notificationProvider={useNotificationProvider}
>
<Routes>
<Route
element={
<ThemedLayout>
<Outlet />
</ThemedLayout>
}
>
<Route index element={<NavigateToResource />} />
<Route path="/posts">
<Route index element={<PostList />} />
<Route path="create" element={<PostCreate />} />
<Route path="edit/:id" element={<PostEdit />} />
<Route path="show/:id" element={<PostShow />} />
</Route>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
</Refine>
</ConfigProvider>
</BrowserRouter>
);
};
render(<App />);
We currently require an upload endpoint that accepts multipart uploads. This address should be passed into the action property of the Upload component.
{
"file": "binary"
}
:::caution
This end-point should be Content-type: multipart/form-data and Form Data: file: binary.
:::
This end-point should respond similarly.
{
"url": "https://example.com/uploaded-file.jpeg"
}
:::caution
We have to use the getValueFromEvent method to convert the uploaded files to Antd UploadFile object.
:::
This data is sent to the API when the form is submitted.
{
"title": "Test",
"image": [
{
"uid": "rc-upload-1620630541327-7",
"name": "greg-bulla-6RD0mcpY8f8-unsplash.jpg",
"url": "https://refine.ams3.digitaloceanspaces.com/78c82c0b2203e670d77372f4c20fc0e2",
"type": "image/jpeg",
"size": 70922,
"percent": 100,
"status": "done"
}
]
}
:::caution
The following data are required for the Antd Upload component and all should be saved.
:::
| Property | Description |
|---|---|
| uid | Unique id |
| name | File Name |
| url | Download URL |
| status | error, success, done, uploading, removed |
Let's add the image field to the post editing form.
import {
// highlight-start
useApiUrl,
// highlight-end
} from "@refinedev/core";
import {
// highlight-start
getValueFromEvent,
// highlight-end
Edit,
useForm,
} from "@refinedev/antd";
import {
// highlight-next-line
Upload,
Form,
Input,
} from "antd";
export const PostEdit: React.FC = () => {
const { formProps, saveButtonProps } = useForm<IPost>();
// highlight-next-line
const apiUrl = useApiUrl();
return (
<Edit saveButtonProps={saveButtonProps}>
<Form {...formProps} layout="vertical">
<Form.Item
label="Title"
name="title"
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item label="Image">
<Form.Item
name="image"
valuePropName="fileList"
// highlight-next-line
getValueFromEvent={getValueFromEvent}
noStyle
>
// highlight-start
<Upload.Dragger
name="file"
action={`${apiUrl}/media/upload`}
listType="picture"
maxCount={5}
multiple
>
<p className="ant-upload-text">Drag & drop a file in this area</p>
</Upload.Dragger>
// highlight-end
</Form.Item>
</Form.Item>
</Form>
</Edit>
);
};
setInitialRoutes(["/posts/edit/111"]);
const App = () => {
return (
<BrowserRouter>
<ConfigProvider theme={RefineThemes.Blue}>
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider(API_URL)}
resources={[
{
name: "posts",
list: "/posts",
create: "/posts/create",
show: "/posts/show/:id",
edit: "/posts/edit/:id",
},
]}
notificationProvider={useNotificationProvider}
>
<Routes>
<Route
element={
<ThemedLayout>
<Outlet />
</ThemedLayout>
}
>
<Route index element={<NavigateToResource />} />
<Route path="/posts">
<Route index element={<PostList />} />
<Route path="create" element={<PostCreate />} />
<Route path="edit/:id" element={<PostEdit />} />
<Route path="show/:id" element={<PostShow />} />
</Route>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
</Refine>
</ConfigProvider>
</BrowserRouter>
);
};
render(<App />);
The request, like the one below, is sent for the edit form.
{
"id": 1,
"title": "Test",
"image": [
{
"uid": "rc-upload-1620630541327-7",
"name": "greg-bulla-6RD0mcpY8f8-unsplash.jpg",
"url": "https://refine.ams3.digitaloceanspaces.com/78c82c0b2203e670d77372f4c20fc0e2",
"type": "image/jpeg",
"size": 70922,
"percent": 100,
"status": "done"
}
]
}
This data is sent to the API when form is submitted.
{
"title": "Test",
"image": [
{
"uid": "rc-upload-1620630541327-7",
"name": "greg-bulla-6RD0mcpY8f8-unsplash.jpg",
"url": "https://refine.ams3.digitaloceanspaces.com/78c82c0b2203e670d77372f4c20fc0e2",
"type": "image/jpeg",
"size": 70922,
"percent": 100,
"status": "done"
}
]
}
You may want to disable the "Save" button in the form while the upload is going on. To do this, you can use the useFileUploadState hook.
import { useApiUrl } from "@refinedev/core";
import {
getValueFromEvent,
// highlight-next-line
useFileUploadState,
Create,
useForm,
} from "@refinedev/antd";
import { Upload, Form, Input } from "antd";
export const PostCreate: React.FC = () => {
const { formProps, saveButtonProps } = useForm<IPost>();
// highlight-next-line
const { isLoading, onChange } = useFileUploadState();
const apiUrl = useApiUrl();
return (
<Create
// highlight-start
saveButtonProps={{
...saveButtonProps,
disabled: isLoading,
}}
// highlight-end
>
<Form {...formProps} layout="vertical">
<Form.Item
label="Title"
name="title"
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item label="Image">
<Form.Item
name="image"
valuePropName="fileList"
getValueFromEvent={getValueFromEvent}
noStyle
>
<Upload.Dragger
name="file"
action={`${apiUrl}/media/upload`}
listType="picture"
maxCount={5}
multiple
// highlight-next-line
onChange={onChange}
>
<p className="ant-upload-text">Drag & drop a file in this area</p>
</Upload.Dragger>
</Form.Item>
</Form.Item>
</Form>
</Create>
);
};