Rivo Account Widget SDK
Rivo Account Widget SDK
This guide shows developers how to extend and customize the Rivo Account Widget on a Shopify storefront. It focuses on the SDK surfaces exposed by the widget runtime: global objects, Alpine stores, hash routing, and built‑in JavaScript hooks.
Quick Start
The Account Widget runs on storefront pages and bootstraps an Alpine.js instance with a custom prefix (rivo-ax-). When it loads, these globals are available:
window.RivoProfileAlpine– the Alpine instancewindow.RivoAPI– the Rivo API client (preconfigured for the shop)window.Rivo– global Rivo config and common data
You can use these to read and customize widget state, inject dynamic content, or respond to widget navigation.
Global Objects
window.RivoProfileAlpine
window.RivoProfileAlpineThe Alpine.js instance used by the Account Widget.
- Prefix is set to
rivo-ax-to avoid conflicts with other Alpine instances. - Use stores to access data and settings.
Example:
<span rivo-ax-text="$store.rivo_profile_customer.email"></span>
window.RivoAPI
window.RivoAPIA preconfigured API client for customer and widget data. It’s used by the widget internally and is safe to call from custom hooks.
Common methods used by the widget:
window.RivoAPI.identify_customer("loggedin");
window.RivoAPI.points_logs("loggedin");
window.RivoAPI.orders("loggedin", 10);
window.RivoAPI.order("loggedin", orderId);
window.RivoAPI.order_payment_details("loggedin", orderId);
window.RivoAPI.shopify_customer("loggedin");
window.RivoAPI.preferences();
window.RivoAPI.favorite_products("loggedin", currencyCode);
window.RivoAPI.favorite_collections("loggedin");
window.RivoAPI.update_saved_cart_collection(productIds, "add", currencyCode);
window.RivoAPI.products(productIds, "shopify", currencyCode);
window.RivoAPI.popular_products("shopify", currencyCode);
window.RivoAPI.membership();
window.RivoAPI.gift_card_lookup(giftCardUid);
window.RivoAPI.gift_card_code_uid(code);
window.RivoAPI.referral_stats("loggedin");
window.Rivo
window.RivoRivo runtime configuration and storefront context:
window.Rivo.loy_config– loyalty config + widget settingswindow.Rivo.common.customer– current customer (if logged in)window.Rivo.common.product– current product (if any)window.Rivo.common.collection– current collection (if any)window.Rivo.global_config– global app settings
Alpine Stores (Key Data)
The widget stores its state in Alpine stores. These are the most useful for customization:
| Store | Description |
|---|---|
rivo_profile_customer | Current customer object (Rivo + Shopify data) |
rivo_aw_settings | Account widget settings (feature toggles, text, URLs) |
rivo_aw_translation | Active translation object |
loy_config | Full loyalty config |
icons | SVG icon registry |
rivo_recently_viewed_product_ids | Persisted product IDs |
rivo_recently_viewed_collections | Persisted collection data |
rivo_completed_preference_ids | Persisted preferences |
Example (JS):
const customer = RivoProfileAlpine.store("rivo_profile_customer");
console.log(customer.email);
Custom JavaScript Hooks
The Account Widget exposes built‑in JavaScript hook points via settings.
These hooks are configured in account_widget_settings and executed inside the widget when triggered.
1) onload_javascript
onload_javascriptRuns once after the widget initializes.
Example:
// account_widget_settings.onload_javascript
const customer = RivoProfileAlpine.store("rivo_profile_customer");
if (customer?.tags?.includes("VIP")) {
RivoProfileAlpine.store("rivo_aw_settings").home_logged_out_title = "Welcome back, VIP";
}2) on_hash_change_javascript
on_hash_change_javascriptRuns whenever the widget navigates to a new hash page.
Example:
// account_widget_settings.on_hash_change_javascript
// `page` and `id` are available as local variables
if (page === "rivo-orders") {
console.log("User opened Orders page");
}3) refresh_cart_javascript
refresh_cart_javascriptRuns after the widget adds items to cart (and sometimes after gift card purchase).
Example:
// account_widget_settings.refresh_cart_javascript
document.dispatchEvent(new CustomEvent("cart:refresh"));4) home_button_gift_card_click_action
home_button_gift_card_click_actionRuns when the Gift Card CTA is clicked.
Example:
// account_widget_settings.home_button_gift_card_click_action
window.location.href = "/products/gift-card";5) Login Event Hook (rivo-accounts:shopify:login)
rivo-accounts:shopify:login)When a customer logs in through the widget, a custom event is dispatched on document.
Event payload:
document.dispatchEvent(new CustomEvent('rivo-accounts:shopify:login', {
detail: { customerId: response.customer?.id || response.customer?.remote_id }
}));
Listen for it:
document.addEventListener('rivo-accounts:shopify:login', (event) => {
const { customerId } = event.detail || {};
console.log("Rivo customer logged in:", customerId);
// Example: refresh UI, analytics, or personalization
document.dispatchEvent(new CustomEvent("loyalty:customer:logged_in", { detail: { customerId } }));
});
Deep Linking and Hash Routing
The widget uses window.location.hash to control pages. You can link directly to a widget page by setting the hash.
Base Pages
#rivo#rivo-orders#rivo-profile#rivo-preferences#rivo-favorites#rivo-saved#rivo-shipping-addresses#rivo-add-shipping-address#rivo-buy-gift-card#rivo-gift-card-balance#rivo-alternate-login#rivo-manage-membership#rivo-update-payment-method#rivo-community
Pages With IDs
- Order details:
#rivo-order-details--ORDER_ID - Update shipping address:
#rivo-update-shipping-address--ADDRESS_ID - Survey:
#rivo-survey--SURVEY_HANDLE--QUESTION_ID - Gift card lookup:
#rivo-lookup-gift-card--GIFT_CARD_UID - Favorite collection:
#rivo-favorite-collection--COLLECTION_ID
Auto‑open
You can open the widget automatically by adding:
?show_rivo_account=true
Link Replacement (Account Redirects)
When replace_account_links is enabled, the widget will replace /account links with the widget hash.
To exclude a link from being replaced, add:
<a href="/account" data-rivo-accounts-ignore>Account</a>
You can also customize the replacement URL via:
replace_account_links_url(example:/pages/account#rivo)
Customizing UI Text & Content
The widget reads most copy from:
rivo_aw_translation(locale-specific)rivo_aw_settings(fallback/defaults)
This means you can override text or insert HTML in translations and settings:
<div rivo-ax-html="$store.rivo_aw_translation.featured_home_content"></div>
Example settings commonly used:
featured_home_contentcustom_sub_cta_contenthome_cta_1_text,home_cta_1_url(and 2/3 variants)home_logged_out_titlehome_logged_out_subtitleprimary_button_class_namessecondary_button_class_names
Adding Custom Blocks to the Homepage
You can inject fully custom HTML blocks into the Account Widget homepage using the onload_javascript hook combined with Alpine-powered markup. This lets you display personalized content, promotions, or custom UI that reacts to customer data.
Important: All custom class names should be prefixed with
rivo-to avoid conflicts with theme styles. CSS selectors should be scoped under#rivo-account-slideoutto ensure styles only apply within the widget.
Step 1: Create Your HTML Block
Design your custom block using standard HTML with Rivo's Alpine directives (rivo-ax-*). The block can access any Alpine store data.
<div id="rivo-my-custom-block" class="rivo-custom-block" style="display: none;">
<div class="rivo-custom-block-inner">
<!-- Personalized greeting -->
<h3 rivo-ax-text="'Welcome back, ' + ($store.rivo_profile_customer.first_name || 'friend') + '!'"></h3>
<!-- Points display -->
<div class="rivo-points-summary">
<span>You have </span>
<strong rivo-ax-text="$store.rivo_profile_customer.pretty_points_tally || '0'"></strong>
<span> points</span>
</div>
<!-- Conditional VIP badge -->
<div class="rivo-vip-badge" rivo-ax-show="$store.rivo_profile_customer.current_vip_tier?.name">
<span rivo-ax-text="$store.rivo_profile_customer.current_vip_tier?.name + ' Member'"></span>
</div>
<!-- Points to next reward -->
<p class="rivo-next-reward" rivo-ax-show="$store.rivo_profile_customer.points_to_next_reward > 0">
<span rivo-ax-text="$store.rivo_profile_customer.points_to_next_reward"></span> points until your next reward!
</p>
</div>
</div>Step 2: Inject Using the Onload Hook
Use onload_javascript to insert your block into the widget DOM and initialize it with Alpine:
// account_widget_settings.onload_javascript
// Wait for the widget home container to exist
const waitForHome = setInterval(() => {
const homeContainer = document.querySelector('#rivo-account-slideout .rivo-home-page');
if (!homeContainer) return;
clearInterval(waitForHome);
// Create the custom block
const customBlock = document.createElement('div');
customBlock.innerHTML = `
<div id="rivo-my-custom-block" class="rivo-custom-block">
<div class="rivo-custom-block-inner">
<h3 rivo-ax-text="'Welcome back, ' + ($store.rivo_profile_customer.first_name || 'friend') + '!'"></h3>
<div class="rivo-points-summary">
<span>You have </span>
<strong rivo-ax-text="$store.rivo_profile_customer.pretty_points_tally || '0'"></strong>
<span> points</span>
</div>
<div class="rivo-vip-badge" rivo-ax-show="$store.rivo_profile_customer.current_vip_tier?.name">
<span rivo-ax-text="$store.rivo_profile_customer.current_vip_tier?.name + ' Member'"></span>
</div>
</div>
</div>
`;
// Insert at the top of the home container
homeContainer.prepend(customBlock.firstElementChild);
}, 100);Step 3: Add Custom Styles
Include CSS to style your block. You can add this via theme CSS or inline in the hook:
// Add styles dynamically in onload_javascript
const styles = document.createElement('style');
styles.textContent = `
#rivo-account-slideout .rivo-custom-block {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
color: white;
}
#rivo-account-slideout .rivo-custom-block h3 {
margin: 0 0 12px 0;
font-size: 1.25rem;
}
#rivo-account-slideout .rivo-points-summary {
font-size: 1.1rem;
margin-bottom: 8px;
}
#rivo-account-slideout .rivo-points-summary strong {
font-size: 1.5rem;
}
#rivo-account-slideout .rivo-vip-badge {
display: inline-block;
background: rgba(255,255,255,0.2);
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85rem;
margin-top: 8px;
}
`;
document.head.appendChild(styles);Complete Example: Promotional Block
Here's a full example that shows a promotional block only to customers who qualify:
// account_widget_settings.onload_javascript
// Add styles (scoped to #rivo-account-slideout)
const styles = document.createElement('style');
styles.textContent = `
#rivo-account-slideout .rivo-promo-block {
background: #1a1a2e;
border: 1px solid #4a4a6a;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
color: #fff;
}
#rivo-account-slideout .rivo-promo-block__title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 8px;
}
#rivo-account-slideout .rivo-promo-block__points {
font-size: 2rem;
font-weight: 700;
color: #ffd700;
}
#rivo-account-slideout .rivo-promo-block__cta {
margin-top: 12px;
}
#rivo-account-slideout .rivo-promo-block__cta a {
background: #ffd700;
color: #1a1a2e;
padding: 8px 16px;
border-radius: 6px;
text-decoration: none;
font-weight: 600;
}
`;
document.head.appendChild(styles);
// Wait for home page
const waitForHome = setInterval(() => {
const homeContainer = document.querySelector('#rivo-account-slideout .rivo-home-page');
if (!homeContainer) return;
clearInterval(waitForHome);
const customer = RivoProfileAlpine.store('rivo_profile_customer');
// Only show to logged-in customers with 500+ points
if (!customer?.id || (customer?.points_tally || 0) < 500) return;
const promoBlock = document.createElement('div');
promoBlock.className = 'rivo-promo-block';
promoBlock.innerHTML = `
<div class="rivo-promo-block__title">
<span rivo-ax-text="$store.rivo_profile_customer.first_name"></span>, you're so close!
</div>
<div>
<span class="rivo-promo-block__points" rivo-ax-text="$store.rivo_profile_customer.pretty_points_tally"></span>
<span> points earned</span>
</div>
<div class="rivo-promo-block__cta" rivo-ax-show="$store.rivo_profile_customer.points_tally >= 1000">
<a href="#rivo-rewards">Redeem a Reward</a>
</div>
`;
homeContainer.prepend(promoBlock);
}, 100);Available Customer Fields for Custom Blocks
Use these fields from $store.rivo_profile_customer in your blocks:
| Field | Type | Description |
|---|---|---|
first_name | string | Customer's first name |
last_name | string | Customer's last name |
email | string | Customer's email |
points_tally | number | Raw points balance |
pretty_points_tally | string | Formatted points (e.g., "1,250") |
credits_tally | number | Store credit balance |
pretty_credits_tally | string | Formatted credit (e.g., "$12.50") |
current_vip_tier | object | Current VIP tier (name, id) |
points_to_next_reward | number | Points needed for next reward |
referral_code | string | Customer's referral code |
tags | array | Shopify customer tags |
orders_count | number | Total order count |
total_spent | string | Lifetime spend |
Tips for Custom Blocks
- Always use
rivo-ax-*– The widget uses a custom Alpine prefix to avoid conflicts - Use optional chaining – Customer data may not exist:
$store.rivo_profile_customer?.first_name - Check login state – Use
$store.rivo_profile_customer?.idto verify logged-in status - Wait for the container – Use
setIntervalto wait for the widget DOM to render
Utility Globals
The widget defines a few global helpers you can call:
window.rivoRefreshBalance();
window.rivoRefreshPointsLogs();
These re-fetch customer balances and activity logs.
Best Practices
- Use the
rivo-ax-prefix for all Alpine directives. - Always include fallbacks:
customer?.first_name || 'there'. - Avoid heavy synchronous work in hooks.
- Keep injected HTML minimal and safe.
- Prefer
pretty_points_tally/pretty_credits_tallyfor display.
Troubleshooting
Alpine directive does nothing
- Make sure you used
rivo-ax-, notx-.
Customer fields are missing
- Customer data loads only when logged in.
- Some fields are optional; use fallbacks.
Custom hook not firing
- Confirm the setting is enabled in
account_widget_settings. - Check for JS errors in the browser console.
FAQ
Can I use rivo_profile_customer outside the widget?
No. It only exists inside Account Widget pages. Use the Rivo JS SDK or API elsewhere.
How do I test custom hooks?
Add temporary console.log statements and open the widget to confirm runtime behavior.
Reference: Customer Store
For a full customer field reference, see the advanced customization guide or inspect:
console.log(RivoProfileAlpine.store("rivo_profile_customer"));
Customizing Outside of the Rivo Account Widget
The Rivo SDK isn't limited to customizing the Account Widget itself. You can use Rivo's customer data to personalize any part of your storefront page—announcement bars, hero sections, promotional banners, or any other element.
How It Works
- Access customer data via
RivoProfileAlpine.store("rivo_profile_customer") - Select any DOM element on your page using standard selectors
- Update content dynamically based on customer status, order history, or loyalty data
Key Data Sources
| Source | Description | Available |
|---|---|---|
RivoProfileAlpine.store("rivo_profile_customer") | Full customer object with Rivo + Shopify data | After widget loads |
RivoProfileAlpine.store("rivo_aw_settings") | Widget settings and feature toggles | After widget loads |
RivoProfileAlpine.store("loy_config") | Loyalty program configuration | After widget loads |
localStorage.getItem('rivo_profile_hint') | Cached profile data (even logged out) | After first identification |
rivo:profile-hint-identified event | Fires when profile hint is set | On page load |
Accessing Customer Data
Via JavaScript:
// Get the customer store
const customer = RivoProfileAlpine.store("rivo_profile_customer");
// Access customer properties
console.log(customer.email);
console.log(customer.first_name);
console.log(customer.points_tally);
console.log(customer.pretty_points_tally);
console.log(customer.current_vip_tier?.name);Via HTML with Alpine directives:
You can also bind customer data directly in your HTML using the rivo-ax- prefix and $store:
<!-- Display customer name -->
<span rivo-ax-text="$store.rivo_profile_customer.first_name"></span>
<!-- Display points balance -->
<p>You have <strong rivo-ax-text="$store.rivo_profile_customer.pretty_points_tally"></strong> points</p>
<!-- Conditionally show content for logged-in customers -->
<div rivo-ax-show="$store.rivo_profile_customer.id">
Welcome back, <span rivo-ax-text="$store.rivo_profile_customer.first_name"></span>!
</div>
<!-- Show VIP tier badge if customer has one -->
<span class="vip-badge" rivo-ax-show="$store.rivo_profile_customer.current_vip_tier?.name"
rivo-ax-text="$store.rivo_profile_customer.current_vip_tier?.name + ' Member'"></span>
<!-- Display credit balance -->
<div rivo-ax-show="$store.rivo_profile_customer.credits_tally > 0">
Store credit: <span rivo-ax-text="$store.rivo_profile_customer.pretty_credits_tally"></span>
</div>Note: Elements using
rivo-ax-directives must be on pages where the Rivo Account Widget is loaded. The Alpine instance (RivoProfileAlpine) initializes when the widget loads.
Quick Start Example
This example shows how to personalize your page by replacing a hero title and injecting a VIP tier badge.
Where to add this code: Accounts → Account Widget → Javascript Settings for Developers → Onload Javascript
// Wait for the widget to initialize
setTimeout(() => {
const customer = RivoProfileAlpine.store("rivo_profile_customer");
if (!customer?.id) return; // Exit if not logged in
// 1. Replace hero title with personalized greeting
const heroTitle = document.querySelector('.hero-container h1');
if (heroTitle) {
heroTitle.textContent = `Hello, ${customer.first_name || 'there'}!`;
}
// 2. Inject VIP tier section below the hero subheading
const heroSubheading = document.querySelector('.hero-container .subheading');
if (heroSubheading && customer.current_vip_tier?.name) {
const tierSection = document.createElement('div');
tierSection.className = 'rivo-vip-section';
tierSection.innerHTML = `
<p class="rivo-vip-message">You're a <strong>${customer.current_vip_tier.name}</strong> member</p>
`;
heroSubheading.insertAdjacentElement('afterend', tierSection);
}
}, 500);Tip: Adjust the selector (
.hero-container h1,.hero-container .subheading) to match your theme's actual HTML structure.
Example: Campaign Configuration
Here's a pattern for running personalized campaigns across your entire storefront:
// Define your campaign configuration
window.Rivo.loyalty_campaign_enabled = true;
window.Rivo.loyalty_campaign_config = {
discount_code: "LOYALTY20",
desktop_announcement_text: "VIP Exclusive: Extra 20% off with code LOYALTY20",
mobile_announcement_text: "VIP: 20% off. Code: LOYALTY20",
sign_in_text: "an exclusive 20% discount",
hero_title_customer: "Welcome back! Your exclusive offer awaits",
hero_title_guest: "Sign in to unlock your VIP discount",
hero_body: "As a valued customer, enjoy an extra 20% off your next order."
};
// Set up campaign state based on customer eligibility
function setCampaignConfig() {
if (window.Rivo.loyalty_campaign_enabled) {
const customer = RivoProfileAlpine.store("rivo_profile_customer");
window.Rivo.campaign_config = {
profile_hint: JSON.parse(localStorage.getItem('rivo_profile_hint') || '{}'),
discount_code: window.Rivo.loyalty_campaign_config.discount_code,
is_mobile: window.innerWidth < 768
};
// Check if customer is eligible (e.g., has previous orders)
window.Rivo.campaign_config.discount_code_eligible = (
window.Rivo.campaign_config?.profile_hint?.customer_orders_count > 0 ||
customer?.orders_count > 0
);
} else {
window.Rivo.campaign_config = {};
}
}
setCampaignConfig();Example: Personalized Announcement Bar
Update your theme's announcement bar with personalized messaging:
function updateAnnouncementBar() {
const announcementText = document.querySelector('.announcement-bar__text');
if (!announcementText) return;
const customer = RivoProfileAlpine.store("rivo_profile_customer");
const firstName = customer?.first_name ||
window.Rivo.campaign_config?.profile_hint?.first_name;
// Add smooth transition
announcementText.style.transition = 'opacity 0.3s ease-out';
announcementText.style.opacity = '0';
setTimeout(() => {
if (customer?.id) {
// Logged-in customer: show discount code
const text = window.Rivo.campaign_config.is_mobile
? window.Rivo.loyalty_campaign_config.mobile_announcement_text
: window.Rivo.loyalty_campaign_config.desktop_announcement_text;
announcementText.innerHTML = `<strong>${text}</strong>`;
} else {
// Guest: prompt to sign in
announcementText.innerHTML = `<strong>${firstName ? `${firstName}, ` : ''}<a href="#rivo">Sign in</a> to unlock ${window.Rivo.loyalty_campaign_config.sign_in_text}</strong>`;
}
announcementText.style.opacity = '1';
}, 300);
}
// Run on eligible customers
if (window.Rivo.campaign_config.discount_code_eligible) {
updateAnnouncementBar();
}Example: Dynamic Hero Section
Personalize a hero section based on customer loyalty data:
function updateHeroSection() {
const heroContainer = document.querySelector('.hero-section');
if (!heroContainer) return;
const customer = RivoProfileAlpine.store("rivo_profile_customer");
if (!customer?.first_order_at) return;
// Calculate days since first order
const firstOrderDate = new Date(customer.first_order_at);
const today = new Date();
const diffDays = Math.floor(Math.abs(today - firstOrderDate) / (1000 * 60 * 60 * 24));
const formattedDays = String(diffDays).padStart(3, '0');
const firstName = customer.first_name;
// Update hero content
const header = heroContainer.querySelector('.hero-header');
const subtext = heroContainer.querySelector('.hero-subtext');
if (header) {
header.textContent = `DAY ${formattedDays}`;
}
if (subtext) {
subtext.textContent = `${firstName ? `Hey ${firstName}, ` : ''}Thanks for being a loyal customer for ${diffDays} days!`;
}
}
// Wait for customer data to load
const checkCustomerInterval = setInterval(() => {
const customer = RivoProfileAlpine.store("rivo_profile_customer");
if (customer?.first_order_at) {
updateHeroSection();
clearInterval(checkCustomerInterval);
}
}, 100);
// Clear after 20 seconds to prevent infinite loop
setTimeout(() => clearInterval(checkCustomerInterval), 20000);Example: Conditional Content Based on VIP Tier
Show different content based on the customer's VIP tier:
function showTierContent() {
const tierBanner = document.querySelector('.vip-tier-banner');
if (!tierBanner) return;
const customer = RivoProfileAlpine.store("rivo_profile_customer");
const currentTier = customer?.current_vip_tier?.name;
if (currentTier === 'Gold' || currentTier === 'Platinum') {
tierBanner.innerHTML = `
<div class="tier-message">
<h3>${customer.first_name}, you've unlocked ${currentTier} benefits!</h3>
<p>Enjoy free shipping and early access to sales.</p>
</div>
`;
tierBanner.style.display = 'block';
} else {
tierBanner.style.display = 'none';
}
}Listening for Profile Identification
For logged-out visitors who have been previously identified, listen for the profile hint event:
document.addEventListener('rivo:profile-hint-identified', () => {
console.log('Profile hint identified');
setCampaignConfig();
const customer = RivoProfileAlpine.store("rivo_profile_customer");
const discountCodeEligible =
window.Rivo.campaign_config?.profile_hint?.customer_orders_count > 0;
if (!customer?.id && discountCodeEligible) {
// Logged-out returning customer—show personalized content
updateAnnouncementBar();
updateHeroSection();
}
});Auto-Applying Discount Codes
Automatically apply a discount code for eligible customers:
function applyDiscountCode(code) {
// Check if already applied (via cookie)
const currentCode = document.cookie
.split('; ')
.find(row => row.startsWith('discount_code='))
?.split('=')[1];
if (currentCode !== code) {
// Shopify discount URL pattern
fetch(`/discount/${code}`);
}
}
if (window.Rivo.campaign_config.discount_code_eligible) {
applyDiscountCode(window.Rivo.campaign_config.discount_code);
}Best Practices for Page Customization
- Use
RivoProfileAlpine.store("rivo_profile_customer")– This is the primary way to access customer data - Use intervals with timeouts – Customer data may load asynchronously; always set a max wait time
- Check for elements before modifying – Use
if (!element) return;guards - Check login state with
customer?.id– Use optional chaining to verify the customer is logged in - Add smooth transitions – Avoid jarring content changes with CSS transitions
- Combine with profile hints – Use localStorage hints to personalize even before full login
- Listen for Rivo events –
rivo:profile-hint-identifiedandrivo-accounts:shopify:loginlet you react to state changes - Scope selectors carefully – Use specific selectors to avoid unintended changes
Updated 4 days ago