docs/docs/docs/01-core/content-manager/04-relations.mdx
Relations are a term used to describe how two or more entities are connected. Previously in the sidebar of an entity, in Nov2020 we released a refactor that moved these fields into the main editing flow for a better editor experience and to improve performance of the CMS application when many relations were used.
above: An example of the relations input in the CMS edit view
above: A high-level diagram of how relations state management works
When you first open an existing entity, we call the admin API and put the data into the store to pre-populate fields
with existing values. However, its important to know when you have fields with type === 'relation' in your schema
that the data you receive will not be an array, but rather an object with the count of how many relations in that
field exist. For example, a section of the response may look like this:
{
"my_relations": {
"count": 6
}
}
So without intervention, your inputs would try to append new relations to the my_relations object, which would not
work. Instead of this, before calling the redux action INIT_FORM we recursively find the paths fields based on the
following conditions:
These paths do not take into account index values. So if you have a repetable component field where the schema looks like:
{
"repeatable_single_component_relation": {
"type": "component",
"repeatable": true,
"component": "basic.relation"
}
}
and the components looks like:
{
"basic.relation": {
"attributes": {
"id": {
"type": "integer"
},
"categories": {
"type": "relation",
"relation": "oneToMany",
"target": "api::category.category",
"targetModel": "api::category.category",
"relationType": "oneToMany"
},
"my_name": {
"type": "string"
}
}
}
}
Then the path to the relation field would be repeatable_single_component_relation.categories. Even though when
relations are added the path to the field in the redux store would be repeatable_single_component_relation.0.categories.
Inside the reducer we reduce the array of relationalFieldPaths to an object with the initialValues clone as
as the base. If there is modifiedData in the browser i.e. you've made changes to the entity and saved those changes,
we just replace the first level of the field with the modifiedData so the data structure is preserved and we're not
loosing the relations we had already loaded in the component. If the first part of the path is highlighted as the
relationalField then we simply replace that intial object with an empty array.
However, if the first part of the path is either a repeatable component, a dynamic zone or a regular component then we
recursively find the relation fields and replace the object with an array. This is handled by the findLeafByPathAndReplace
utility function. This function in short, takes an end path (in this case the relational field) and a primitive to replace
when it finds the endpath (an empty array in this case). It then recursively reduces the paths to the relational field mapping
through arrays if necessary (in the instance of repetable components for example) replacing the endpath with the primitive.
When this is done, we have sucessfully prepared our initial data for usage with relations.
Because we've prepared the fields prior to the component loading, adding & removing relations, it's relatively easy to do so.
When a relation is added, we simply push the new relation to the array of relations. When a relation is removed, we simply
filter out the relation from the array of relations. This is handled inside the reducer actions CONNECT_RELATION &
DISCONNECT_RELATION respectively.
:::note Connecting relations adds the item to the end of the list, whilst loading more relations prepends to the beginning of the list. This is the expected behaviour, to keep the order of the list in the UI in sync with the API response. :::
The RelationInput component takes the field in modifiedData as its source of truth. You could therefore consider this to
be the browserState and initialData to be the serverState. When relations are loaded they're added to both the intialData
and modifiedData objects, but when you connect/disconnect only the modifiedData is updated. This is useful when we're preparing
data for the api.
The API to update the entity expects relations to be categorised into two groups, a connect array and disconnect array.
You could do this as the user interacts with the input but we found this to be confusing and then involved us managing three
different arrays which makes the code more complex. Instead, because the browser doesn't really care about whats new and removed
and we have a copy of the slice of data we're mutating from the server we can run a small diff algorithm to determine which
relations have been connected and which have been disconnected. Returning an object like so:
{
"my_relations": {
"connect": [{ "id": 1 }, { "id": 2 }],
"disconnect": []
}
}
The input field for relation fields consist of two components:
RelationInputDataManagerThis container component handles data fetching and data normalization for the RelationInput component. This has been extracted from
the RelationInput so that Strapi is able to move the underlying component into the design-system if the community would need it
(most other input components can be consumed from there).
RelationInputThis component is the presentational counterpart to the RelationInputDataManager component. It renders an input field based on the data passed from the data manager.
Under the hood it is using react-window to render a list of relations in a virtualized view. Some fields need to render thousands of relations, which
would otherwise have a negative impact on the overall performance of the content-manager.
This hook takes care of data-fetching and normalizes results relations aswell as search-results.
const { relations: RelationResults, search: RelationResults, searchFor } = useRelation(reactQueryCacheKey: Array<string | object>, options: Options);
Optionsoptions is a mandatory configuration and should implement the following shape:
type Options = {
name: string; // name of the relation field
relation: RelationConfiguration;
search: SearchConfiguration;
}
type RelationConfiguration = {
endpoint: string; // URL from where existing relations should be fetched
enabled: boolean; // defines whether relations should be fetched once the hook is called
pageParams: object; // additional query params which will be appended to `endpoint`
onLoad: (results: RelationResult[]) => void; // callback that will be fired after relations have been fetched (paginated)
normalizeArguments = {
mainFieldName: string; // name of the target model main field, determining which field to display (fallback: id)
shouldAddLink: boolean; // if the user is allowed to read the target model, the returned relations should include a link to the target
targetModel: object; // target content-type model
};
pageGoal: number; // the current page-count of the already loaded relations used to keep the redux store and query cache in sync.
}
type SearchConfiguration = {
endpoint: string; // URL from where new relations should be fetched
pageParams: object; // additional query params which will be appended to `endpoint`
}
relations and search both return a consistent relation format:
type RelationResults = RelationResult[];
type RelationResult = {
id: number;
href?: string; // based on `shouldAddLink` and the `targetModel`
publicationState: 'draft' | 'published';
mainField: string; // will fallback to "id" if not set
};
relationsrelations refers to a inifinite-query return type from react-query. It exposes paginated relational data
aswell as methods to check if there are more pages or fetch more paginated results. Relations for a given field are fetched as soon as the hook is called.
searchsearch refers to a inifinite-query return type from react-query. It exposes paginated search results
for a relational field. Search results are only fetched after searchFor() has been called.
searchFor(string)searchFor is a method which can be used to search for entities which haven't been connected with the source entity yet. The method accepts a search-term: searchFor("term").