Coupon
Description
A headless render props component that provides state and actions for applying and removing coupon codes. It handles cart dispatch, validation, loading states, and error management — but renders no UI of its own.
Role in Theming
Coupon is one of EverShop's headless components — it owns the coupon business logic while leaving all UI decisions to its parent:
- Coupon renders nothing. It returns only what its
childrenfunction renders. - Theme developers do not override Coupon. Instead, they override the parent components that consume it (
CouponForm, or the cart/checkout pages that include them). - The logic stays stable across themes. Coupon validation, apply/remove dispatch, error handling, and state tracking are all encapsulated in Coupon.
Theme Override Points
Coupon is consumed by these components, which are the actual override targets for theme developers:
| Parent Component | Route | Override Path in Theme |
|---|---|---|
| CouponForm | cart, checkout | themes/<name>/src/pages/cart/CouponForm.tsx |
| CartTotalSummary (Discount section) | cart, checkout | themes/<name>/src/pages/cart/ShoppingCart.tsx |
Import
import { Coupon } from '@components/frontStore/Coupon';
Usage
import { Coupon } from '@components/frontStore/Coupon';
import { useState } from 'react';
function CouponInput() {
const [code, setCode] = useState('');
return (
<Coupon>
{(state, actions) => (
<div>
{state.hasActiveCoupon ? (
<div>
<span>Applied: {state.appliedCoupon}</span>
<button
onClick={actions.removeCoupon}
disabled={state.isLoading}
>
Remove
</button>
</div>
) : (
<div>
<input
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Enter coupon code"
/>
<button
onClick={() => actions.applyCoupon(code)}
disabled={!state.canApplyCoupon || state.isLoading}
>
Apply
</button>
</div>
)}
{state.error && <p className="error">{state.error}</p>}
</div>
)}
</Coupon>
);
}
Props
| Name | Type | Required | Description |
|---|---|---|---|
| onApplySuccess | (couponCode: string) => void | No | Callback when coupon is applied successfully |
| onRemoveSuccess | () => void | No | Callback when coupon is removed successfully |
| onError | (error: string) => void | No | Callback on error |
| children | (state: CouponState, actions: CouponActions) => ReactNode | Yes | Render function receiving state and actions |
State Object
The render function receives a state object:
interface CouponState {
isLoading: boolean; // True during any cart operation
error: string | null; // Error message if any
appliedCoupon: string | null; // Currently applied coupon code
canApplyCoupon: boolean; // True if a coupon can be applied (cart ready, no active coupon)
canRemoveCoupon: boolean; // True if a coupon can be removed (cart ready, has active coupon)
hasActiveCoupon: boolean; // True if a coupon is currently applied
}
Actions Object
The render function receives an actions object:
interface CouponActions {
applyCoupon: (code: string) => Promise<void>; // Apply a coupon code
removeCoupon: () => Promise<void>; // Remove the applied coupon
clearError: () => void; // Clear error state
}
Examples
Basic Coupon Form
import { Coupon } from '@components/frontStore/Coupon';
import { useState } from 'react';
function BasicCouponForm() {
const [code, setCode] = useState('');
return (
<Coupon
onApplySuccess={(applied) => {
setCode('');
}}
>
{(state, actions) => {
if (state.hasActiveCoupon) {
return (
<div className="applied-coupon">
<span>Coupon: {state.appliedCoupon}</span>
<button
onClick={actions.removeCoupon}
disabled={state.isLoading}
>
Remove
</button>
</div>
);
}
return (
<form
onSubmit={(e) => {
e.preventDefault();
actions.applyCoupon(code);
}}
>
<input
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Coupon code"
disabled={state.isLoading}
/>
<button
type="submit"
disabled={!state.canApplyCoupon || state.isLoading || !code.trim()}
>
{state.isLoading ? 'Applying...' : 'Apply'}
</button>
{state.error && (
<p className="error">
{state.error}
<button onClick={actions.clearError}>Dismiss</button>
</p>
)}
</form>
);
}}
</Coupon>
);
}
Theme Override Example
A theme developer overrides CouponForm to provide a custom coupon input experience. Create this file in your theme:
themes/my-theme/src/pages/cart/CouponForm.tsx
import { Coupon } from '@components/frontStore/Coupon';
import { useState } from 'react';
function CouponForm() {
const [code, setCode] = useState('');
const [expanded, setExpanded] = useState(false);
return (
<Coupon
onApplySuccess={() => {
setCode('');
setExpanded(false);
}}
onError={(error) => {
// Custom error handling
}}
>
{(state, actions) => {
if (state.hasActiveCoupon) {
return (
<div className="my-theme-coupon-applied">
<span>Discount applied: {state.appliedCoupon}</span>
<button onClick={actions.removeCoupon} disabled={state.isLoading}>
Remove
</button>
</div>
);
}
if (!expanded) {
return (
<button
className="my-theme-coupon-toggle"
onClick={() => setExpanded(true)}
>
Have a coupon code?
</button>
);
}
return (
<div className="my-theme-coupon-form">
<input
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Enter code"
/>
<button
onClick={() => actions.applyCoupon(code)}
disabled={!state.canApplyCoupon || state.isLoading}
>
{state.isLoading ? 'Applying...' : 'Apply'}
</button>
<button onClick={() => setExpanded(false)}>Cancel</button>
{state.error && <p>{state.error}</p>}
</div>
);
}}
</Coupon>
);
}
export default CouponForm;
Features
- Headless: Renders no UI — full control via render props
- Apply/Remove: Full coupon lifecycle management
- Validation: Checks cart readiness and duplicate coupon application
- Loading State: Automatic loading state management
- Error Handling: Built-in error management with clearError action
- Callbacks: onApplySuccess, onRemoveSuccess, and onError hooks
- Type Safe: Full TypeScript support
- Cart Integration: Uses CartContext automatically
Related Components
- CartContext - Shopping cart context
- CartTotalSummary - Cart totals (uses Coupon in discount section)
- AddToCart - Add to cart component
Support us
EverShop is an open-source project that relies on community support. If you find our project useful, please consider sponsoring us.