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.

Make sure you are committing the session (in case it changes) after calling the rechargeQueryWrapper function in the response from your loader/action.

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',
'Set-Cookie': await context.rechargeSession.commit(),
},
}
);
}

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',
'Set-Cookie': await context.rechargeSession.commit(),
},
}
);
}

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
}
}
`;