This is the early access documentation preview for Custom Views. This documentation might not be in sync with our official documentation.
Data Fetching
In client-side applications, data fetching is not always easy and it usually involves a lot of boilerplate code around implementation, state management, data normalization, etc.
A Custom View uses the Merchant Center API to connect to any of the Composable Commerce APIs.
To make it easier to connect to these APIs, we provide some pre-configured HTTP clients for both GraphQL and REST APIs.
GraphQL HTTP client
The Composable Commerce APIs and the Merchant Center have first-class support for GraphQL. We strongly recommend building your Custom View using GraphQL whenever possible.
Custom Views provide a pre-configured Apollo GraphQL Client to perform requests to the GraphQL APIs.
A custom instance of the Apollo client can be created using the createApolloClient function.
Using React hooks
The preferred way of fetching data in a React component is to use React hooks.
The Apollo Client provides some React hooks to query data and mutate data.
Using the hooks in your React component is as simple as this:
import { useQuery } from '@apollo/client/react';import { GRAPHQL_TARGETS } from '@commercetools-frontend/constants';const Channels = (props) => {const { data } = useQuery(FetchChannelsQuery, {context: {target: GRAPHQL_TARGETS.COMMERCETOOLS_PLATFORM,}});return (<div>{/* Do something with `data` */}</div>);};
Notice here that we define the context.target
. This is how you configure the GraphQL target for the Merchant Center API.
The available GraphQL targets are exposed as a GRAPHQL_TARGETS
constant in the @commercetools-frontend/constants
package.
Using GraphQL documents
In Custom Views you define your queries and mutations in dedicated .graphql
files that you can import
in your component file.
Using .graphql
files is a great way to co-locate your data requirements next to your React component. Furthermore, you can leverage these files to improve the developer experience with editor syntax highlighting, linting, code generation, etc.
Let's create one:
query FetchChannels {channels {totalcountoffsetresults {idkeyrolesnameAllLocales {localevalue}}}}
And import it from the React component file:
import FetchChannelsQuery from './fetch-channels.ctp.graphql';
Error handling
Of course we need to handle situations where requests fail for whatever reason (network errors, schema validation errors, etc.).
The Apollo Client hooks provide an error
object that we can use in our component to handle failures.
For example, we can render a content notification error instead of rendering the component.
import { useQuery } from '@apollo/client/react';import { GRAPHQL_TARGETS } from '@commercetools-frontend/constants';import { ContentNotification } from '@commercetools-uikit/notifications';import Text from '@commercetools-uikit/text';const Channels = (props) => {const { data, error } = useQuery(FetchChannelsQuery, {context: {target: GRAPHQL_TARGETS.COMMERCETOOLS_PLATFORM,}});if (error) {return (<ContentNotification type="error"><Text.Body>{getErrorMessage(error)}</Text.Body></ContentNotification>);}return (<div>{/* Do something with `data` */}</div>);};
For more information about using the error
from Apollo Client, see Error handling.
In our case we just want to print the error.graphQLErrors
. We can attempt to do that by implementing a helper function like this:
const getErrorMessage = (error) =>error.graphQLErrors?.map((e) => e.message).join('\n') || error.message;
REST HTTP client
Some endpoints or APIs might not be available as GraphQL but as a standard HTTP REST endpoint instead.
Custom Views provide a pre-configured declarative fetching library @commercetools-frontend/sdk
, which builds on top of the JS SDK client and Redux
The SDK library is built using Redux actions, meaning that you dispatch an action describing the request and the library takes care of handling the request.
import { useAsyncDispatch, actions } from '@commercetools-frontend/sdk';import { MC_API_PROXY_TARGETS } from '@commercetools-frontend/constants';const Channels = (props) => {const dispatch = useAsyncDispatch();useEffect(() => {async function execute() {try {const result = await dispatch(actions.get({mcApiProxyTarget: MC_API_PROXY_TARGETS.COMMERCETOOLS_PLATFORM,service: 'channels',}););// Update state with `result`} catch (error) {// Update state with `error`}}execute();}, [dispatch])return (<div>{/* Do something with the state */}</div>);};
Notice here that we define the mcApiProxyTarget
. This is how you configure the proxy target for the Merchant Center API.
The available proxy targets are exposed as a MC_API_PROXY_TARGETS
constant in the @commercetools-frontend/constants
package.
The SDK library does not include features like data normalization, caching, etc. You will need to build these on your own.
Custom HTTP client
By default Custom Views provide pre-configured HTTP clients for GraphQL and REST API requests.
However, you could use any other HTTP client of your choice, for example Fetch, Axios, Stale-While-Revalidate (SWR), etc.
The main problem with that is the fact that you need to configure these clients on your own, in particular regarding the HTTP headers that should be sent with every request.
To make it easier to configure your HTTP client with the necessary HTTP headers, we provide some dedicated utility functions, in particular the executeHttpClientRequest.
Example using Fetch
The example below uses the Fetch API.
import createHttpUserAgent from '@commercetools/http-user-agent';import {buildApiUrl,executeHttpClientRequest,} from '@commercetools-frontend/application-shell';const userAgent = createHttpUserAgent({name: 'fetch-client',version: '1.0.0',libraryName: window.app.applicationName,contactEmail: 'support@my-company.com',});const fetcher = async (url, config = {}) => {const data = await executeHttpClientRequest(async (options) => {const res = await fetch(buildApiUrl(url), options);const data = await res.json();return {data,statusCode: res.status,getHeader: (key) => res.headers.get(key),};},{ userAgent, headers: config.headers });return data;};const Channels = () => {useEffect(() => {async function execute() {try {const result = await fetcher('/proxy/ctp/channels');// Update state with `result`} catch (error) {// Update state with `error`}}execute();}, [])// ...};
Example using Axios
The example below uses the Axios HTTP client.
import axios from 'axios';import createHttpUserAgent from '@commercetools/http-user-agent';import {buildApiUrl,executeHttpClientRequest,} from '@commercetools-frontend/application-shell';const userAgent = createHttpUserAgent({name: 'axios-client',version: '1.0.0',libraryName: window.app.applicationName,contactEmail: 'support@my-company.com',});const fetcher = async (url, config = {}) => {const data = await executeHttpClientRequest(async (options) => {const res = await axios(buildApiUrl(url), {...config,headers: options.headers,withCredentials: options.credentials === 'include',});return {data: res.data,statusCode: res.status,getHeader: (key) => res.headers[key],};},{ userAgent, headers: config.headers });return data;};const Channels = () => {useEffect(() => {async function execute() {try {const result = await fetcher('/proxy/ctp/channels');// Update state with `result`} catch (error) {// Update state with `error`}}execute();}, [])// ...};
Example using SWR
The example below uses the SWR React hook.
import useSWR from 'swr';import createHttpUserAgent from '@commercetools/http-user-agent';import {buildApiUrl,executeHttpClientRequest,} from '@commercetools-frontend/application-shell';const userAgent = createHttpUserAgent({name: 'swr-client',version: '1.0.0',libraryName: window.app.applicationName,contactEmail: 'support@my-company.com',});const fetcher = async (url, config = {}) => {const data = await executeHttpClientRequest(async (options) => {const res = await fetch(buildApiUrl(url), options);const data = await res.json();return {data,statusCode: res.status,getHeader: (key) => res.headers.get(key),};},{ userAgent, headers: config.headers });return data;};const Channels = () => {const { data, error } = useSWR('/proxy/ctp/channels', fetcher);// ...};
Example using Ky
The example below uses the Ky HTTP client.
import ky from 'ky';import createHttpUserAgent from '@commercetools/http-user-agent';import {buildApiUrl,executeHttpClientRequest,} from '@commercetools-frontend/application-shell';const userAgent = createHttpUserAgent({name: 'ky-client',version: '1.0.0',libraryName: window.app.applicationName,contactEmail: 'support@my-company.com',});const fetcher = async (url, config = {}) => {const data = await executeHttpClientRequest(async (options) => {const res = await ky(buildApiUrl(url), options);const data = await res.json();return {data,statusCode: res.statusCode,getHeader: (key) => res.headers.get(key),};},{ userAgent, headers: config.headers });return data;};const Channels = () => {useEffect(() => {async function execute() {try {const result = await fetcher('/proxy/ctp/channels');// Update state with `result`} catch (error) {// Update state with `error`}}execute();}, [])// ...};
Advanced usage
Using Connector Hooks
As your data requirements grow, for example by having multiple queries and mutations in a React component, or using the same query in multiple components, you might want to extract your queries into separate reusable hooks.
We recommend extracting the logic into what we call connector hooks.
A connector hook is a React hook that implements most of the data requirements (queries and mutations) specific to the use cases where the data should be used.
For example, we can extract the fetching of Channels into a connector hook named useChannelsFetcher
.
This hook can be put into a file named use-channels-connector.js
.
import { useQuery } from '@apollo/client/react';import { GRAPHQL_TARGETS } from '@commercetools-frontend/constants';export const useChannelsFetcher = () => {const { data, error, loading } = useQuery(FetchChannelsQuery, {context: {target: GRAPHQL_TARGETS.COMMERCETOOLS_PLATFORM,}});return {channels: data?.channels,error,loading,};};
Note that the use-channels-connector.js
file can contain multiple connector hooks. For example useChannelsFetcher
, useChannelsUpdater
, useChannelsCreator
, and so on.
We would then use our connector hook in the Channels
component instead of directly using the Apollo Client hooks. Therefore, there is less code in the React component as most of the logic and configuration is abstracted away in the connector hook.
import { ContentNotification } from '@commercetools-uikit/notifications';import Text from '@commercetools-uikit/text';import { useChannelsFetcher } from '../../hooks/use-channels-connector';const Channels = (props) => {const { channels, error } = useChannelsFetcher();if (error) {return (<ContentNotification type="error"><Text.Body>{getErrorMessage(error)}</Text.Body></ContentNotification>);}return (<div>{/* Do something with `channels` */}</div>);};
Connecting to an external API
In case your Custom View needs to connect to an external API, in addition to the Composable Commerce APIs, the HTTP client has to be reconfigured to connect to the /proxy/forward-to
endpoint with the appropriate headers.
When using the Apollo client, you need to pass some configuration options to the context
object.
The @commercetools-frontend/application-shell
package now exposes a createApolloContextForProxyForwardTo to construct a predefined context object specific to the /proxy/forward-to
.
import {createApolloContextForProxyForwardTo,useMcQuery,} from '@commercetools-frontend/application-shell';import Text from '@commercetools-uikit/text';import HelloWorldQuery from './hello-world.graphql';const HelloWorld = () => {const { loading, data, error } = useMcQuery(HelloWorldQuery, {context: createApolloContextForProxyForwardTo({uri: 'https://my-custom-app.com/graphql',}),});if (loading) return 'Loading...';if (error) return `Error! ${error.message}`;return <Text.Headline as="h1">{data.title}</Text.Headline>;}
Similarly, when using a custom HTTP client, you can pass an additional configuration object forwardToConfig
.
import createHttpUserAgent from '@commercetools/http-user-agent';import {buildApiUrl,executeHttpClientRequest,} from '@commercetools-frontend/application-shell';const fetcherForwardTo = async (targetUrl, config = {}) => {const data = await executeHttpClientRequest(async (options) => {const res = await fetch(buildApiUrl('/proxy/forward-to'), options);const data = res.json();return {data,statusCode: res.status,getHeader: (key) => res.headers.get(key),};},{userAgent,headers: config.headers,forwardToConfig: {uri: targetUrl}});return data;};const HelloWorld = () => {useEffect(() => {async function execute() {try {const result = await fetcherForwardTo('https://my-custom-app.com/api');// Update state with `result`} catch (error) {// Update state with `error`}}execute();}, [])// ...}
Calling REST APIs inside your GraphQL queries
This feature is available from version 21.10.0
onwards.
In case you want to keep using the benefits of GraphQL (and Apollo Client) but for REST APIs, you can use the Apollo REST link as an opt-in feature to the GraphQL HTTP client.
You can follow the instructions of the Apollo REST link to set it up and configure the GraphQL document to send a request via the @rest
directive.
To use the Apollo REST link with the built-in GraphQL HTTP client you need to create a new instance of Apollo Client using the createApolloClient utility function.
Here is an example of fetching Channels using the @rest
directive.
import { useMcQuery } from '@commercetools-frontend/application-shell';import { useApplicationContext } from '@commercetools-frontend/application-shell-connectors';import { MC_API_PROXY_TARGETS } from '@commercetools-frontend/constants';import {usePaginationState,useDataTableSortingState,} from '@commercetools-uikit/hooks';import { FetchChannelsQuery } from './fetch-channels.ctp.graphql';const Channels = (props) => {const projectKey = useApplicationContext(context => context.project.key);const { page, perPage } = usePaginationState();const tableSorting = useDataTableSortingState({ key: 'key', order: 'asc' });const searchParams = new URLSearchParams({limit: perPage.value,offset: (page.value - 1) * perPage.value,sort: `${tableSorting.value.key} ${tableSorting.value.order}`,});const { data, error, loading } = useMcQuery(FetchChannelsQuery, {fetchPolicy: 'cache-and-network',variables: {endpoint: `/proxy/${MC_API_PROXY_TARGETS.COMMERCETOOLS_PLATFORM}/${projectKey}/channels?${searchParams.toString()}`,},context: {skipGraphQlTargetCheck: true,},});if (error) {return (<ContentNotification type="error"><Text.Body>{getErrorMessage(error)}</Text.Body></ContentNotification>);}return (<div>{/* Do something with `channels` */}</div>);};
And here is the related GraphQL document:
query FetchChannelsRest($endpoint: String!) {channels @rest(type: "ChannelQueryResult", path: $endpoint) {totalcountoffsetresults @type(name: "Channel") {idkeynameAllLocales @type(name: "LocalizedString") {localevalue}}}}
A few things to note:
Types and nested types require to define the
__typename
via@rest(type: "")
or@type(name: "")
.The
endpoint
is passed as a variable and should point to the Merchant Center API proxy target endpoint.For
LocalizedString
fields such asname
(in the REST API) you should use the GraphQL naming conventionnameAllLocales
. However, you also need to instruct the Apollo REST link to correctly transform the shape of the field. You can do that using the responseTransformer function.import { applyTransformedLocalizedStrings } from '@commercetools-frontend/l10n';const restLink = new RestLink({uri: getMcApiUrl(),// https://www.apollographql.com/docs/react/api/link/apollo-link-rest/#response-transformingresponseTransformer: async (response, typeName) => {const data = await response.json();switch (typeName) {case 'ChannelQueryResult':return {...data,results: data.results.map((channel) =>applyTransformedLocalizedStrings(channel, [{from: 'name',to: 'nameAllLocales',},])),};default:return data;}},})
Using file uploads
In case you need to deal with file uploads, you will probably use FormData
.
The FormData
object can then be sent as the HTTP request body.
It is important to properly configure the Content-Type
HTTP header, which usually must be set to multipart/form-data
. However, you also need to provide a boundary
directive.
We recommend to omit the Content-Type
HTTP header in the request and let the browser correctly infer it.
When using the @commercetools-frontend/sdk
package, you need to explicitly unset the Content-Type
HTTP header by passing null
as value.
{'Content-Type': null}