Data Fetching
When fetching data via the Recharge JavaScript SDK in Hydrogen we recommend always fetching data within a loader
/action
.
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.
- JavaScript
- TypeScript
/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
</NavLink>
|
<NavLink to="/account/subscriptions" style={isActiveStyle}>
Subscriptions
</NavLink>
|
<NavLink to="/account/profile" style={isActiveStyle}>
Profile
</NavLink>
|
<NavLink to="/account/addresses" style={isActiveStyle}>
Addresses
</NavLink>
|
<Logout />
</nav>
);
}
/app/routes/account.tsx
function AccountMenu() {
function isActiveStyle({
isActive,
isPending,
}: {
isActive: boolean;
isPending: boolean;
}) {
return {
fontWeight: isActive ? 'bold' : undefined,
color: isPending ? 'grey' : 'black',
};
}
return (
<nav role="navigation">
<NavLink to="/account/orders" style={isActiveStyle}>
Orders
</NavLink>
|
<NavLink to="/account/subscriptions" style={isActiveStyle}>
Subscriptions
</NavLink>
|
<NavLink to="/account/profile" style={isActiveStyle}>
Profile
</NavLink>
|
<NavLink to="/account/addresses" style={isActiveStyle}>
Addresses
</NavLink>
|
<Logout />
</nav>
);
Fetch Subscriptions and Render Subscription List
Example subscriptions list page that renders a list of Subscriptions with links to a details page.
- JavaScript
- TypeScript
/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'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 />
</>
);
}
/app/routes/account.subscriptions._index.tsx
import type { MetaFunction } from '@remix-run/react';
import { Link, useLoaderData } from '@remix-run/react';
import { Money } from '@shopify/hydrogen';
import type { LoaderFunctionArgs } from '@shopify/remix-oxygen';
import { json } from '@shopify/remix-oxygen';
import { rechargeQueryWrapper } from '~/lib/rechargeUtils';
import type { Subscription } from '@rechargeapps/storefront-client';
import { listSubscriptions } from '@rechargeapps/storefront-client';
import type { CurrencyCode } from '@shopify/hydrogen/storefront-api-types';
export const meta: MetaFunction = () => {
return [{ title: 'Subscriptions' }];
};
/**
* @param {LoaderFunctionArgs}
*/
export async function loader({ context }: LoaderFunctionArgs) {
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() {
const {
subscriptionsResponse: { subscriptions },
} = useLoaderData<typeof loader>();
return (
<div className="subscriptions">
{subscriptions.length ? <SubscriptionsTable subscriptions={subscriptions} /> : <EmptySubscriptions />}
</div>
);
}
/**
* @param {{subscriptions: import('@rechargeapps/storefront-client').Subscription[]}}
*/
function SubscriptionsTable({ subscriptions }: { subscriptions: Subscription[] }) {
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'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 }: { subscription: 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') as CurrencyCode,
}}
/>
<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.
- JavaScript
- TypeScript
/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
}
}
`;
/app/routes/account.subscriptions.$id.tsx
import type { LoaderFunctionArgs } from '@shopify/remix-oxygen';
import { json, redirect } from '@shopify/remix-oxygen';
import type { MetaFunction } from '@remix-run/react';
import { useLoaderData, NavLink } from '@remix-run/react';
import { Money } from '@shopify/hydrogen';
import { rechargeQueryWrapper } from '~/lib/rechargeUtils';
import { getSubscription, getActiveChurnLandingPageURL } from '@rechargeapps/storefront-client';
import type { CurrencyCode } from '@shopify/hydrogen/storefront-api-types';
export const meta: MetaFunction<typeof loader> = ({ data }) => {
return [{ title: `Subscription ${data?.subscription?.id}` }];
};
/**
* @param {LoaderFunctionArgs}
*/
export async function loader({ request, params, context }: LoaderFunctionArgs) {
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, subscriptionId, 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() {
const { subscription, cancelUrl, product } = useLoaderData<typeof loader>();
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') as CurrencyCode,
}}
/>
</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
}
}
`;