Skip to main content

Data Fetching

When fetching data via the Recharge JavaScript SDK in Hydrogen we recommend always fetching data within a loader/action.

info

Make sure to set the 'Cache-Control': 'no-cache, no-store, must-revalidate', header in your loader/action responses to prevent any caching between users as well.

Update account nav

Update the AccountMenu component in account.jsx to add a subscriptions link.

/app/routes/account.jsx

function AccountMenu() {
function isActiveStyle({ isActive, isPending }) {
return {
fontWeight: isActive ? 'bold' : undefined,
color: isPending ? 'grey' : 'black',
};
}

return (
<nav role="navigation">
<NavLink to="/account/orders" style={isActiveStyle}>
Orders &nbsp;
</NavLink>
&nbsp;|&nbsp;
<NavLink to="/account/subscriptions" style={isActiveStyle}>
&nbsp; Subscriptions &nbsp;
</NavLink>
&nbsp;|&nbsp;
<NavLink to="/account/profile" style={isActiveStyle}>
&nbsp; Profile &nbsp;
</NavLink>
&nbsp;|&nbsp;
<NavLink to="/account/addresses" style={isActiveStyle}>
&nbsp; Addresses &nbsp;
</NavLink>
&nbsp;|&nbsp;
<Logout />
</nav>
);
}

Fetch Subscriptions and Render Subscription List

Example subscriptions list page that renders a list of Subscriptions with links to a details page.

/app/routes/account.subscriptions._index.jsx

import { Link, useLoaderData } from '@remix-run/react';
import { Money } from '@shopify/hydrogen';
import { json } from '@shopify/remix-oxygen';
import { rechargeQueryWrapper } from '~/lib/rechargeUtils';
import { listSubscriptions } from '@rechargeapps/storefront-client';

/**
* @type {MetaFunction}
*/
export const meta = () => {
return [{ title: 'Subscriptions' }];
};

/**
* @param {LoaderFunctionArgs}
*/
export async function loader({ context }) {
const subscriptionsResponse = await rechargeQueryWrapper(
session =>
listSubscriptions(session, {
limit: 25,
status: 'active',
}),
context
);

return json(
{ subscriptionsResponse },
{
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
}
);
}

export default function Subscriptions() {
/** @type {LoaderReturnData} */
const {
subscriptionsResponse: { subscriptions },
} = useLoaderData();
return (
<div className="subscriptions">
{subscriptions.length ? <SubscriptionsTable subscriptions={subscriptions} /> : <EmptySubscriptions />}
</div>
);
}

/**
* @param {{subscriptions: import('@rechargeapps/storefront-client').Subscription[]}}
*/
function SubscriptionsTable({ subscriptions }) {
return (
<div className="acccount-subscriptions">
{subscriptions.length ? (
subscriptions.map(subscription => <SubscriptionItem key={subscription.id} subscription={subscription} />)
) : (
<EmptySubscriptions />
)}
</div>
);
}

function EmptySubscriptions() {
return (
<div>
<p>You haven&apos;t placed any subscriptions yet.</p>
<br />
<p>
<Link to="/collections">Start Shopping</Link>
</p>
</div>
);
}

/**
* @param {{subscription: import('@rechargeapps/storefront-client').Subscription}}
*/
function SubscriptionItem({ subscription }) {
return (
<>
<fieldset>
<Link to={`/account/subscriptions/${btoa(subscription.id)}`}>
<strong>#{subscription.id}</strong>
</Link>
<p>{`${subscription.product_title}${subscription.variant_title ? ` (${subscription.variant_title})` : ''}`}</p>
<p>{new Date(subscription.created_at).toDateString()}</p>
<p>{subscription.status}</p>
<p>{`${subscription.quantity} Every ${subscription.order_interval_frequency} ${subscription.order_interval_unit}(s)`}</p>
<Money
data={{
amount: subscription.price,
currencyCode: subscription.presentment_currency ?? 'USD',
}}
/>
<Link to={`/account/subscriptions/${btoa(subscription.id)}`}>View Subscription</Link>
</fieldset>
<br />
</>
);
}

Subscription Details Route

Example subscriptions detail page that renders Subscription Details with a link to Active Churn Recovery landing page flow.

/app/routes/account.subscriptions.$id.jsx

import { json, redirect } from '@shopify/remix-oxygen';
import { useLoaderData, NavLink } from '@remix-run/react';
import { Money } from '@shopify/hydrogen';
import { rechargeQueryWrapper } from '~/lib/rechargeUtils';
import { getSubscription, getActiveChurnLandingPageURL } from '@rechargeapps/storefront-client';

/**
* @type {MetaFunction<typeof loader>}
*/
export const meta = ({ data }) => {
return [{ title: `Subscription ${data?.subscription?.id}` }];
};

/**
* @param {LoaderFunctionArgs}
*/
export async function loader({ request, params, context }) {
if (!params.id) {
return redirect('/account/subscriptions');
}

const subscriptionId = atob(params.id);
const subscription = await rechargeQueryWrapper(
session =>
getSubscription(session, subscriptionId, {
include: ['address'],
}),
context
);

if (!subscription) {
throw new Error('Subscription not found');
}

const cancelUrl = await rechargeQueryWrapper(
session => getActiveChurnLandingPageURL(session, params.id, request.url),
context
);

const { product } = await context.storefront.query(PRODUCT_QUERY, {
variables: {
id: `gid://shopify/Product/${subscription.external_variant_id.ecommerce}`,
},
});

return json(
{
subscription,
cancelUrl,
product,
},
{
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
}
);
}

export default function SubscriptionRoute() {
/** @type {LoaderReturnData} */
const { subscription, cancelUrl, product } = useLoaderData();
const address = subscription.include?.address;
return (
<div className="account-subscription">
<h2>Subscription No. {subscription.id}</h2>
<p>Placed on {new Date(subscription.created_at).toDateString()}</p>
<br />
<div>
<table>
<thead>
<tr>
<th scope="col">Product</th>
<th scope="col">Price</th>
<th scope="col">Quantity</th>
<th scope="col">Frequency</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div>
<NavLink to={`/products/${product?.handle}`}>View Product</NavLink>
<p>{subscription.product_title}</p>
{subscription.variant_title && <small>{subscription.variant_title}</small>}
</div>
</td>
<td>
<Money
data={{
amount: subscription.price,
currencyCode: subscription.presentment_currency ?? 'USD',
}}
/>
</td>
<td>{subscription.quantity}</td>
<td>{`Every ${subscription.order_interval_frequency} ${subscription.order_interval_unit}(s)`}</td>
</tr>
</tbody>
</table>
<div>
<h3>Shipping Address</h3>
{address ? (
<address>
<p>
{address.first_name && address.first_name + ' '}
{address.last_name}
</p>
<p>{address.address1}</p>
{address.address2 && <p>{address.address2}</p>}
<p>
{address.city} {address.province} {address.zip} {address.country_code}
</p>
</address>
) : (
<p>No shipping address defined</p>
)}
<h3>Status</h3>
<div>
<p>{subscription.status}</p>
</div>
</div>
</div>
<br />
{subscription.status === 'active' && <a href={cancelUrl}>Cancel Subscription</a>}
</div>
);
}

const PRODUCT_QUERY = `#graphql
query getProductById($id: ID!) {
product(id: $id) {
id
handle
}
}
`;