Skip to main content
Version: 4.xx.xx
Swizzle Ready
Source Code

Strapi v4

Refine supports the features that come with Strapi-v4.

A few of the Strapi-v4 API features are as follows:

  • Fields Selection
  • Relations Population
  • Publication State
  • Locale

meta allows us to use the above features in hooks. Thus, we can fetch the data according to the parameters we want.

Hooks and components that support meta:

NOTE

There is no need to use meta for sorting, pagination, and, filters. Sorting, pagination, and, filters will be handled automatically by the strapi-v4 dataProvider.

INFORMATION

Normally, strapi-v4 backend returns data in the following format:

{
"id": 1,
"attributes": {
"title": "My title",
"content": "Long content...",
}

However, we can use normalizeData to customize the data returned by the backend. So, our data will look like:

{
"id": 1,
"title": "My title",
"content": "Long content..."
}

Setup​

npm i @refinedev/strapi-v4
CAUTION

To make this example more visual, we used the @refinedev/antd package. If you are using Refine headless, you need to provide the components, hooks, or helpers imported from the @refinedev/antd package.

Usage​

App.tsx
import { Refine } from "@refinedev/core";
import { DataProvider } from "@refinedev/strapi-v4";

const App: React.FC = () => {
return (
<Refine
dataProvider={DataProvider("API_URL")}
/* ... */
>
{/* ... */}
</Refine>
);
};

API Parameters​

Let's examine how API parameters that come with Strapi-v4 are used with meta. Then, let's see how it is used in the application.

Create Collections​

We created two collections on Strapi as posts and categories and added a relation between them. For detailed information on how to create a collection, you can check here.

posts has the following fields:

  • id
  • title
  • content
  • category
  • createdAt
  • locale

Fields Selection​

To select only some fields, we must specify these fields with `meta``.

Refer to the Fields Selection documentation for detailed information. β†’

Get only id and title of all posts
const { tableProps } = useTable<IPost>({
meta: {
fields: ["id", "title"],
},
});
Get all fields of all posts(id, title, category, content ...)
const { tableProps } = useTable<IPost>({
meta: {
fields: "*",
},
});

When sending the request, we can specify which fields will come, so we send fields in meta to hooks that we will fetch data from. In this way, you can perform the queries of only the fields you want.

localhost:5173
// src/pages/posts/list.tsx

import { List, EditButton, ShowButton, useTable } from "@refinedev/antd";
import { Table, Space } from "antd";

const PostList = () => {
const { tableProps, sorter } = useTable<IPost>({
meta: {
fields: ["id", "title"],
},
});

return (
<List>
<Table {...tableProps} rowKey="id">
<Table.Column dataIndex="id" title="ID" />
<Table.Column dataIndex="title" title="Title" />
<Table.Column
title="Actions"
dataIndex="actions"
render={(_, record) => (
<Space>
<EditButton hideText size="small" recordItemId={record.id} />
<ShowButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</List>
);
};

Relations Population​

By default, relations are not populated when fetching entries.

The populate parameter is used to define which fields will be populated.

Refer to the Relations Population documentation for detailed information. β†’

Get all the posts and populate the selected relations
const { tableProps } = useTable<IPost>({
meta: {
populate: ["category", "cover"],
},
});
Get all posts and populate all their first-level relations
const { tableProps } = useTable<IPost>({
meta: {
populate: "*",
},
});

It should be noted that Strapi-V4 allows populating relations more than 1 level.

Get all posts and populate one second-level relation and first-level relation
const { tableProps } = useTable<IPost>({
meta: {
populate: {
category: {
populate: ["cover"],
},
cover: {
populate: [""],
},
},
},
});

In order to pull the categories related to the posts, we can now show the categories in our list by defining the meta populate parameter.

localhost:5173
// src/pages/posts/list.tsx

import {
List,
EditButton,
ShowButton,
useSelect,
FilterDropdown,
useTable,
} from "@refinedev/antd";
import {
Table,
Select,
Space,
} from "antd";

const PostList = () => {
const { tableProps, sorter } = useTable<IPost>({
meta: {
fields: ["id", "title"],
populate: ["category"],
},
});

const { selectProps } = useSelect({
resource: "categories",
optionLabel: "title",
optionValue: "id",
});

return (
<List>
<Table {...tableProps} rowKey="id">
<Table.Column dataIndex="id" title="ID" />
<Table.Column dataIndex="title" title="Title" />
<Table.Column
dataIndex={["category", "title"]}
title="Category"
filterDropdown={(props) => (
<FilterDropdown {...props}>
<Select style={{ minWidth: 200 }} mode="multiple" placeholder="Select Category" {...selectProps} />
</FilterDropdown>
)}
/>
<Table.Column
title="Actions"
dataIndex="actions"
render={(_, record) => (
<Space>
<EditButton hideText size="small" recordItemId={record.id} />
<ShowButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</List>
);
};
Relations Population for /me request​

If you need to the population for the /me request you can use it like this in your authProvider.

const strapiAuthHelper = AuthHelper(API_URL + "/api");

strapiAuthHelper.me("token", {
meta: {
populate: ["role"],
},
});

Publication State​

NOTE

The Draft & Publish feature should be enabled on Strapi.

Refer to the Publication State documentation for detailed information. β†’

live: returns only published entries

preview: returns draft and published entries

const { tableProps } = useTable<IPost>({
meta: {
publicationState: "preview",
},
});

We can list the posts separately according to the published or draft information.

localhost:5173
// src/pages/posts/list.tsx

import { List, EditButton, ShowButton, useSelect, FilterDropdown, useTable } from "@refinedev/antd";
import {
Table,
Space,
Select,
Form,
Radio,
Tag,
} from "antd";

const PostList = () => {
const [publicationState, setPublicationState] = React.useState("live");

const { tableProps, sorter } = useTable<IPost>({
meta: {
fields: ["id", "title", "publishedAt"],
populate: ["category"],
publicationState,
},
});

const { selectProps } = useSelect({
resource: "categories",
optionLabel: "title",
optionValue: "id",
});

return (
<List>
<Form
style={{
marginBottom: 16,
display: "flex",
justifyContent: "center",
gap: "16px",
}}
layout="inline"
initialValues={{
publicationState,
}}
>
<Form.Item label="Publication State" name="publicationState">
<Radio.Group onChange={(e) => setPublicationState(e.target.value)}>
<Radio.Button value="live">Published</Radio.Button>
<Radio.Button value="preview">Draft and Published</Radio.Button>
</Radio.Group>
</Form.Item>
</Form>
<Table {...tableProps} rowKey="id">
<Table.Column dataIndex="id" title="ID" />
<Table.Column dataIndex="title" title="Title" />
<Table.Column
dataIndex={["category", "title"]}
title="Category"
filterDropdown={(props) => (
<FilterDropdown {...props}>
<Select style={{ minWidth: 200 }} mode="multiple" placeholder="Select Category" {...selectProps} />
</FilterDropdown>
)}
/>
<Table.Column
dataIndex="publishedAt"
title="Status"
render={(value) => {
return <Tag color={value ? "green" : "blue"}>{value ? "Published" : "Draft"}</Tag>;
}}
/>
<Table.Column
title="Actions"
dataIndex="actions"
render={(_, record) => (
<Space>
<EditButton hideText size="small" recordItemId={record.id} />
<ShowButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</List>
);
};

Locale​

TIP

To fetch content for a locale, make sure it has been already added to Strapi in the admin panel

Refer to the Locale documentation for detailed information. β†’

const { tableProps } = useTable<IPost>({
meta: {
locale: "de",
},
});

With the local parameter feature, we can fetch posts and categories created according to different languages.

import { useState } from "react";

import {
List,
useTable,
getDefaultSortOrder,
FilterDropdown,
useSelect,
EditButton,
DeleteButton,
} from "@refinedev/antd";
import { Table, Select, Space, Form, Radio, Tag } from "antd";

import { IPost } from "interfaces";

import { API_URL } from "../../constants";

export const PostList: React.FC = () => {
const [locale, setLocale] = useState("en");
const [publicationState, setPublicationState] = useState("live");

const { tableProps, sorter } = useTable<IPost>({
meta: {
populate: ["category", "cover"],
locale,
publicationState,
},
});

const { selectProps } = useSelect({
resource: "categories",
optionLabel: "title",
optionValue: "id",
meta: { locale },
});

return (
<List>
<Form
layout="inline"
initialValues={{
locale,
publicationState,
}}
>
<Form.Item label="Locale" name="locale">
<Radio.Group onChange={(e) => setLocale(e.target.value)}>
<Radio.Button value="en">English</Radio.Button>
<Radio.Button value="de">Deutsch</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="Publication State" name="publicationState">
<Radio.Group onChange={(e) => setPublicationState(e.target.value)}>
<Radio.Button value="live">Published</Radio.Button>
<Radio.Button value="preview">Draft and Published</Radio.Button>
</Radio.Group>
</Form.Item>
</Form>
<br />
<Table
{...tableProps}
rowKey="id"
pagination={{
...tableProps.pagination,
showSizeChanger: true,
}}
>
<Table.Column
dataIndex="id"
title="ID"
defaultSortOrder={getDefaultSortOrder("id", sorter)}
sorter={{ multiple: 3 }}
/>
<Table.Column
dataIndex="title"
title="Title"
defaultSortOrder={getDefaultSortOrder("title", sorter)}
sorter={{ multiple: 2 }}
/>
<Table.Column
dataIndex={["category", "title"]}
title="Category"
filterDropdown={(props) => (
<FilterDropdown {...props}>
<Select style={{ minWidth: 200 }} mode="multiple" placeholder="Select Category" {...selectProps} />
</FilterDropdown>
)}
/>
<Table.Column dataIndex="locale" title="Locale" />
<Table.Column
dataIndex="publishedAt"
title="Status"
render={(value) => {
return <Tag color={value ? "green" : "blue"}>{value ? "Published" : "Draft"}</Tag>;
}}
/>
<Table.Column<{ id: string }>
title="Actions"
render={(_, record) => (
<Space>
<EditButton hideText size="small" recordItemId={record.id} />
<DeleteButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</List>
);
};
localhost:5173
// src/pages/posts/list.tsx

import { List, EditButton, ShowButton, useSelect, FilterDropdown, useTable } from "@refinedev/antd";
import { Table, Space, Select, Form, Radio, Tag } from "antd";

const PostList = () => {
const [locale, setLocale] = React.useState("en");
const [publicationState, setPublicationState] = React.useState("live");
const { tableProps, sorter } = useTable<IPost>({
meta: {
fields: ["id", "title", "publishedAt", "locale"],
populate: ["category"],
locale,
publicationState,
},
});

const { selectProps } = useSelect({
resource: "categories",
optionLabel: "title",
optionValue: "id",
meta: { locale },
});

return (
<List>
<Form
style={{
marginBottom: 16,
display: "flex",
justifyContent: "center",
gap: "16px",
}}
layout="inline"
initialValues={{
locale,
publicationState,
}}
>
<Form.Item label="Locale" name="locale">
<Radio.Group onChange={(e) => setLocale(e.target.value)}>
<Radio.Button value="en">English</Radio.Button>
<Radio.Button value="de">Deutsch</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="Publication State" name="publicationState">
<Radio.Group onChange={(e) => setPublicationState(e.target.value)}>
<Radio.Button value="live">Published</Radio.Button>
<Radio.Button value="preview">Draft and Published</Radio.Button>
</Radio.Group>
</Form.Item>
</Form>
<Table {...tableProps} rowKey="id">
<Table.Column dataIndex="id" title="ID" />
<Table.Column dataIndex="title" title="Title" />
<Table.Column
dataIndex={["category", "title"]}
title="Category"
filterDropdown={(props) => (
<FilterDropdown {...props}>
<Select style={{ minWidth: 200 }} mode="multiple" placeholder="Select Category" {...selectProps} />
</FilterDropdown>
)}
/>
<Table.Column
dataIndex="publishedAt"
title="Status"
render={(value) => {
return <Tag color={value ? "green" : "blue"}>{value ? "Published" : "Draft"}</Tag>;
}}
/>
<Table.Column dataIndex="locale" title="Locale" />
<Table.Column
title="Actions"
dataIndex="actions"
render={(_, record) => (
<Space>
<EditButton hideText size="small" recordItemId={record.id} />
<ShowButton hideText size="small" recordItemId={record.id} />
</Space>
)}
/>
</Table>
</List>
);
};

meta Usages​

When creating and editing posts you can use these API parameters in meta:

const { formProps, saveButtonProps, queryResult } = useForm<IPost>({
meta: { publicationState: "preview" },
});
EditList.tsx
const { formProps, saveButtonProps, queryResult } = useForm<IPost>({
meta: { populate: ["category", "cover"] },
});
CreateList.tsx
const { selectProps } = useSelect({
meta: { locale: "en" },
});

File Upload​

Strapi supports file upload. Below are examples of how to upload files to Strapi.

Refer to the Strapi documentation for more information β†’

getValueProps and mediaUploadMapper are helper functions for Ant Design Form.

import { Edit, useForm } from "@refinedev/antd";
import { getValueProps, mediaUploadMapper } from "@refinedev/strapi-v4";
import { Form, Upload } from "antd";

import { TOKEN_KEY, API_URL } from "../../constants";
import { IPost } from "../interfaces";

export const PostEdit: React.FC = () => {
const { formProps, saveButtonProps } = useForm<IPost>({
metaData: { populate: ["cover"] },
});

return (
<Edit saveButtonProps={saveButtonProps}>
<Form
{...formProps}
layout="vertical"
onFinish={(values) => {
formProps.onFinish?.(mediaUploadMapper(values));
}}
>
<Form.Item label="Cover">
<Form.Item
name="cover"
valuePropName="fileList"
getValueProps={(data) => getValueProps(data, API_URL)}
noStyle
>
<Upload.Dragger
name="files"
action={`${API_URL}/api/upload`}
headers={{
Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`,
}}
listType="picture"
multiple
>
<p className="ant-upload-text">Drag & drop a file in this area</p>
</Upload.Dragger>
</Form.Item>
</Form.Item>
</Form>
</Edit>
);
};

Server-side form validation​

Strapi provides a way to add validation rules to your models. So if you send a request to the server with invalid data, Strapi will return errors for each field that has a validation error.

Refer to the Strapi documentation for more information β†’

By default, @refinedev/strapi-v4 transforms the error response from Strapi into a HttpError object. This object contains the following properties:

  • statusCode - The status code of the response.
  • message - The error message.
  • errors - An object containing the validation errors for each field.

Thus, useForm will automatically set the error message for each field that has a validation error.

Refer to the server-side form validation documentation for more information β†’ .

Example​

Demo Credentials

Username: demo@refine.dev

Password: demodemo

Run on your local
npm create refine-app@latest -- --example data-provider-strapi-v4