Skip to content

Commit 0d85650

Browse files
Enhance documentation for working with relationships in jsonapi-react-tools
1 parent 86ca1e3 commit 0d85650

1 file changed

Lines changed: 153 additions & 69 deletions

File tree

docs/docs/integrations/react-tools.md

Lines changed: 153 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -137,93 +137,177 @@ JsonApiToolkit supports rich querying via URL parameters. `jsonapi-react-tools`
137137

138138
### Working with Relationships (Included Resources)
139139

140-
When your API response includes related resources (using the `include` query parameter), `jsonapi-react-tools` provides type safety for the `included` array. This requires defining a central registry of all your resource types.
140+
JsonApiToolkit responses separate primary resource data from related resources, which are returned in an `included` array. The primary resources relationships contain only resource identifiers. To work with full resource objects and ensure type safety, you must first define your resource attributes and create a central type registry that maps resource type strings to these attribute interfaces.
141+
142+
#### Define Resource Attributes & Create a Type Registry
143+
144+
First, define interfaces for each resource's attributes:
145+
146+
```ts
147+
export interface CompanyAttributes {
148+
companyName: string;
149+
companyCode: string;
150+
establishedAt: string;
151+
}
152+
153+
export interface LocationAttributes {
154+
address: string;
155+
city: string;
156+
country: string;
157+
}
158+
159+
export interface EmployeeAttributes {
160+
firstName: string;
161+
lastName: string;
162+
email: string;
163+
}
164+
```
141165

142-
1. **Define Attribute Types for All Resources:** Ensure you have attribute types defined not only for your primary resource (e.g., `CompanyAttributes`) but also for any resources that might appear in the `included` array (e.g., `LocationAttributes`).
166+
Then, create a registry that maps the resource type strings (as they are returned by your API) to your interfaces:
143167

144-
```ts
145-
// types/Company.ts
146-
export type CompanyAttributes = {
147-
companyName: string;
148-
companyCode: string;
149-
// ... other fields
150-
};
168+
```ts
169+
import { JsonApiTypeRegistry } from "@intility/jsonapi-react-tools";
170+
import { CompanyAttributes } from "./Company";
171+
import { LocationAttributes } from "./Location";
172+
import { EmployeeAttributes } from "./Employee";
151173
152-
// types/Location.ts
153-
export type LocationAttributes = {
154-
address: string;
155-
city: string;
156-
// ... other fields
157-
};
158-
```
174+
export interface AppTypeRegistry extends JsonApiTypeRegistry {
175+
companies: CompanyAttributes;
176+
locations: LocationAttributes;
177+
employees: EmployeeAttributes;
178+
}
179+
```
159180

160-
2. **Create Your Application's Type Registry:** Implement the `JsonApiTypeRegistry` interface, mapping the exact `type` string returned by your API to the corresponding attribute interface.
181+
This registry is used throughout your application to ensure that all JSON:API responses are strongly typed.
161182

162-
```ts
163-
// types/Registry.ts
164-
import { JsonApiTypeRegistry } from "@intility/jsonapi-react-tools";
165-
import { CompanyAttributes } from "./Company";
166-
import { LocationAttributes } from "./Location";
167-
168-
// Add all resource types that can appear in 'data' or 'included'
169-
export interface AppTypeRegistry extends JsonApiTypeRegistry {
170-
companies: CompanyAttributes; // 'companies' is the type string from the API
171-
locations: LocationAttributes; // 'locations' is the type string from the API
172-
}
173-
```
183+
#### 2. Extracting Related Resources
174184

175-
3. **Use the Registry in Your Data Fetching:** Pass your `AppTypeRegistry` as the second generic argument to `JsonApiCollectionDocument` or `JsonApiDocument`.
185+
There are two cases for extracting related resources from a JSON:API response:
176186

177-
```tsx
178-
import { useQuery } from "@tanstack/react-query";
179-
import {
180-
JsonApiCollectionDocument,
181-
isResourceType, // Import the type predicate
182-
buildJsonApiQueryString,
183-
JsonApiQueryOptions,
184-
} from "@intility/jsonapi-react-tools";
185-
import { CompanyAttributes } from "~/types/Company.ts";
186-
import { AppTypeRegistry } from "~/types/Registry.ts"; // Import your registry
187+
**A. Single Resource Responses:**
188+
When fetching a single resource (using a `JsonApiDocument`), all related resources are still available in the `included` array. In this case, you can use the built-in `getIncludedOfType` helper to filter the `included` array by resource type. For example:
187189

188-
const queryOptions: JsonApiQueryOptions<CompanyAttributes> = {
189-
include: ["locations"], // Request related locations
190-
};
191-
const queryString = buildJsonApiQueryString(queryOptions);
190+
```tsx
191+
import { getIncludedOfType } from "@intility/jsonapi-react-tools";
192192
193-
export const CompanyListWithLocations = () => {
194-
// Use the registry in the useQuery type definition
195-
const { data: response, error, isLoading } = useQuery<
196-
JsonApiCollectionDocument<CompanyAttributes, AppTypeRegistry> // <--- Pass registry here
197-
>({ queryKey: ["api", "companies", queryString] });
193+
export function CompanyDetail({ companyId }: { companyId: string }) {
194+
const { data: companyDocument, isLoading } = useCompany(companyId);
198195
199-
if (isLoading) return <div>Loading...</div>;
200-
if (error || !response) return <div>Error</div>;
196+
if (isLoading || !companyDocument || !companyDocument.data) {
197+
return <div>Loading company details...</div>;
198+
}
201199
202-
// Process included data safely
203-
response.included?.forEach((resource) => {
204-
// Use the type predicate to check the type and narrow it down
205-
if (isResourceType(resource, "locations")) {
206-
// TypeScript now knows resource.attributes is LocationAttributes
207-
console.log("Included Location City:", resource.attributes.city);
208-
}
209-
// Add checks for other included types if necessary
210-
});
200+
// Use getIncludedOfType to extract related locations and employees
201+
const locations = getIncludedOfType(companyDocument, "locations");
202+
const employees = getIncludedOfType(companyDocument, "employees");
211203
212-
return (
204+
return (
205+
<div>
206+
<h2>{companyDocument.data.attributes.companyName}</h2>
207+
<p>Code: {companyDocument.data.attributes.companyCode}</p>
208+
<p>
209+
Established:{" "}
210+
<FormatDate date={companyDocument.data.attributes.establishedAt} />
211+
</p>
212+
<section>
213+
<h3>Locations</h3>
213214
<ul>
214-
{response.data.map((company) => (
215-
<li key={company.id}>
216-
{company.attributes.companyName}
217-
{/* You can now safely look up and display related data */}
215+
{locations.map((loc) => (
216+
<li key={loc.id}>
217+
{loc.attributes.address}, {loc.attributes.city},{" "}
218+
{loc.attributes.country}
218219
</li>
219220
))}
220221
</ul>
221-
);
222-
};
223-
```
222+
</section>
223+
<section>
224+
<h3>Employees</h3>
225+
<ul>
226+
{employees.map((emp) => (
227+
<li key={emp.id}>
228+
{emp.attributes.firstName} {emp.attributes.lastName} -{" "}
229+
{emp.attributes.email}
230+
</li>
231+
))}
232+
</ul>
233+
</section>
234+
</div>
235+
);
236+
}
237+
```
224238

225-
By defining the `AppTypeRegistry` and using the `isResourceType` type predicate, you gain full type safety when working with related resources included in your API responses.
239+
In this example, the companys related locations and employees are retrieved directly from the `included` array using `getIncludedOfType`.
240+
241+
**B. Collection Responses:**
242+
When dealing with collection responses (using a `JsonApiCollectionDocument`), each primary resource defines its relationships with only resource identifiers. To resolve these identifiers into full resource objects, use the `resolveRelationship` helper. For example, in a component listing companies:
243+
244+
```tsx
245+
import { useCompanies } from "~/hooks/useCompanies";
246+
import { Table } from "@intility/bifrost-react";
247+
import { resolveRelationship } from "@intility/jsonapi-react-tools";
248+
249+
export function CompanyList() {
250+
const { data: companiesDocument, isLoading } = useCompanies();
251+
252+
if (isLoading || !companiesDocument) {
253+
return <div>Loading companies...</div>;
254+
}
255+
256+
return (
257+
<Table>
258+
<Table.Header>
259+
<Table.Row>
260+
<Table.HeaderCell>Company Name</Table.HeaderCell>
261+
<Table.HeaderCell>Locations</Table.HeaderCell>
262+
<Table.HeaderCell>Employees</Table.HeaderCell>
263+
</Table.Row>
264+
</Table.Header>
265+
<Table.Body>
266+
{companiesDocument.data.map((company) => {
267+
// Resolve the 'locations' and 'employees' relationships for each company.
268+
// Note: The relationship names here (e.g., "locations") must match those
269+
// returned by your API.
270+
const companyLocations = company.relationships?.locations?.data
271+
? resolveRelationship(
272+
company.relationships.locations.data,
273+
companiesDocument,
274+
"locations"
275+
)
276+
: [];
277+
const companyEmployees = company.relationships?.employees?.data
278+
? resolveRelationship(
279+
company.relationships.employees.data,
280+
companiesDocument,
281+
"employees"
282+
)
283+
: [];
284+
285+
return (
286+
<Table.Row key={company.id}>
287+
<Table.Cell>{company.attributes.companyName}</Table.Cell>
288+
<Table.Cell>
289+
{companyLocations.map((loc) => loc.attributes.address).join(
290+
", "
291+
)}
292+
</Table.Cell>
293+
<Table.Cell>
294+
{companyEmployees
295+
.map(
296+
(emp) =>
297+
`${emp.attributes.firstName} ${emp.attributes.lastName}`
298+
)
299+
.join(", ")}
300+
</Table.Cell>
301+
</Table.Row>
302+
);
303+
})}
304+
</Table.Body>
305+
</Table>
306+
);
307+
}
308+
```
226309

310+
Here, the `resolveRelationship` helper maps the relationship identifiers (from the primary company resource) to the full location and employee objects from the collections `included` array.
227311

228312
### Further Information
229313

0 commit comments

Comments
 (0)