NFT Store
The NFT store serves as the centralized state‑management solution for all Colored Key NFT data within RitoSwap. Built with Zustand, this store maintains real‑time token information, manages account‑switching scenarios, and persists critical data across browser sessions. The store acts as the single source of truth for NFT‑related UI state throughout the application.
Store Architecture
The NFT store implements a sophisticated state‑management pattern that balances reactivity, persistence, and performance. By leveraging Zustand’s lightweight architecture, the store provides immediate state updates to subscribed components while maintaining a clean, predictable API.
Core State Structure
interface NFTState {
// Core NFT data
hasNFT: boolean
tokenId: number | null
backgroundColor: string | null
keyColor: string | null
// UI state
isLoading: boolean
error: string | null
// Token gate tracking
hasUsedTokenGate: boolean
// Wallet state
currentAddress: Address | null
// Account switching
isSwitchingAccount: boolean
previousData: {
hasNFT: boolean
tokenId: number | null
backgroundColor: string | null
keyColor: string | null
} | null
}
Each field serves a specific purpose in maintaining the application’s state consistency and enabling smooth user experiences across various scenarios.
State Field Purposes
Field | Purpose | Persistence |
---|---|---|
hasNFT | Indicates current NFT ownership status | No – Fetched fresh each session |
tokenId | The owned token’s unique identifier | No – Blockchain source of truth |
backgroundColor | Algorithmically generated background color | No – Derived from token ID |
keyColor | Algorithmically generated key color | No – Derived from token ID |
isLoading | Global loading spinner flag for NFT‑related async operations | No – Transient UI state |
error | UI‑friendly error message from last failed operation | No – Transient UI state |
hasUsedTokenGate | Whether user has accessed gated content | Yes – Persists across sessions |
currentAddress | Currently connected wallet address | No – Wallet connection state |
isSwitchingAccount | Flag set while the UI is waiting for new account data | No – Temporary UI state |
previousData | Snapshot of NFT data shown during an account switch | No – Cleared once switch completes |
isLoading
and error
are UI‑only helpers. They reset to their default values on every page load and are never persisted.
Action Methods
The store exposes a complete set of actions for manipulating state. These actions map one‑to‑one with Zustand setters defined in nftStore.ts
.
Basic State Updates
setHasNFT: (hasNFT: boolean) => void
setTokenData: (
tokenId: number | null,
backgroundColor: string | null,
keyColor: string | null
) => void
setLoading: (isLoading: boolean) => void
setError: (error: string | null) => void
Token Gate Management
setHasUsedTokenGate: (hasUsed: boolean) => void
Account Management
setCurrentAddress: (address: Address | null) => void
setIsSwitchingAccount: (isSwitching: boolean) => void
Account Switching Flow
startAccountSwitch: () => void
completeAccountSwitch: () => void
State Reset
resetState: () => void
All Actions Reference
Action | Purpose |
---|---|
setHasNFT | Toggle ownership flag and trigger UI refresh |
setTokenData | Atomically update tokenId & colors |
setLoading | Global loading spinner control |
setError | Set/clear UI‑friendly error message |
setHasUsedTokenGate | Persist gate usage across sessions |
setCurrentAddress | Track the connected wallet address |
setIsSwitchingAccount | Internal flag toggled by wallet listeners before a switch |
startAccountSwitch | Snapshot current data & enter switch mode |
completeAccountSwitch | Clear snapshot & exit switch mode |
resetState | Clear all transient data while preserving gate usage + address |
Account Switching Flow
Step 1: Detection
The application detects a wallet account change through wagmi’s account‑change events.
Step 2: State Preservation
startAccountSwitch()
captures current NFT data in previousData
and sets isSwitchingAccount
to true.
Step 3: UI Continuity
Components use previousData
to maintain visual state during the transition, preventing jarring UI changes.
Step 4: Data Refresh
Background processes fetch new account data while the UI remains stable.
Step 5: Completion
When new data arrives, setTokenData()
or completeAccountSwitch()
finalizes the transition.
Persistence Strategy
The store implements selective persistence using Zustand’s persist
middleware. This approach balances data freshness with user experience:
persist(
(set, get) => ({
// ... store implementation
}),
{
name: 'nft-storage',
partialize: (state) => ({
// Only persist token gate usage
hasUsedTokenGate: state.hasUsedTokenGate,
}),
}
)
The persistence configuration ensures that only non‑sensitive, user‑experience data persists across sessions. Token ownership and metadata always refresh from the blockchain to maintain accuracy.
Persistence Rationale
The decision to persist only hasUsedTokenGate
reflects careful consideration of security and user experience:
Security – Token ownership must always reflect current blockchain state to prevent displaying incorrect ownership information after transfers or burns.
User Experience – Remembering token‑gate usage improves the experience for returning users by maintaining their progression through gated content even after browser restarts.
Performance – Minimal persistence reduces storage overhead and speeds up state hydration on application load.
Component Integration Patterns
Components integrate with the NFT store through various patterns depending on their requirements:
Read-Only Components
function NFTDisplay() {
const { hasNFT, tokenId, backgroundColor, keyColor } = useNFTStore();
if (!hasNFT) return <EmptyState />;
return (
<div style={{ backgroundColor }}>
<KeyIcon color={keyColor} />
<span>Token #{tokenId}</span>
</div>
);
}
Interactive Components
function MintButton() {
const { setLoading } = useNFTStore();
const { writeContract } = useWriteContract();
const handleMint = async () => {
setLoading(true);
try {
await writeContract({
address: KEY_TOKEN_ADDRESS,
abi: fullKeyTokenAbi,
functionName: 'mint'
});
// State updates handled by useNFTData hook
} catch (error) {
setLoading(false);
}
};
return <button onClick={handleMint}>Mint NFT</button>;
}
Account-Aware Components
function NFTScreen() {
const {
hasNFT,
backgroundColor,
keyColor,
isSwitchingAccount,
previousData
} = useNFTStore();
// Use previous data during account switches
const displayData = isSwitchingAccount && previousData
? previousData
: { hasNFT, backgroundColor, keyColor };
return <NFTVisualization {...displayData} />;
}
State Update Coordination
The NFT store coordinates with the useNFTData
hook to maintain consistency between blockchain state and UI state. This coordination follows a specific pattern:
Blockchain Query – The useNFTData
hook queries the blockchain for current token ownership and metadata using wagmi’s useReadContract
.
API Synchronization – The hook checks token usage status through the Token Status API, which synchronizes blockchain and database state.
Store Updates – The hook updates the NFT store with fresh data, triggering re-renders in subscribed components.
Error Handling – Failed queries update the error state while preserving the last known good state for UI stability.
This separation of concerns keeps the store focused on state management while delegating data fetching to specialized hooks.
Performance Optimization
The NFT store implements several optimizations to ensure smooth performance:
Selective Subscriptions
Zustand’s subscription model ensures components only re-render when their specific subscribed fields change:
// Only re-renders when hasNFT changes
const hasNFT = useNFTStore(state => state.hasNFT);
// Re-renders for any color change
const colors = useNFTStore(state => ({
backgroundColor: state.backgroundColor,
keyColor: state.keyColor
}));
Atomic Updates
The setTokenData
method updates multiple fields atomically, preventing intermediate states that could cause visual glitches:
// Single update prevents multiple re-renders
setTokenData(tokenId, backgroundColor, keyColor);
// Versus multiple updates (avoid this)
setTokenId(tokenId);
setBackgroundColor(backgroundColor);
setKeyColor(keyColor);
Debounced Loading States
Components should debounce rapid loading state changes to prevent UI flashing:
const [debouncedLoading] = useDebounce(isLoading, 200);
Testing Strategies
Testing the NFT store requires understanding its interaction with external systems:
Unit Testing
Test store actions in isolation using Zustand’s testing utilities:
import { renderHook, act } from '@testing-library/react-hooks';
import { useNFTStore } from '@/app/store/nftStore';
test('account switching preserves data', () => {
const { result } = renderHook(() => useNFTStore());
// Set initial state
act(() => {
result.current.setTokenData(1, '#FF0000', '#00FF00');
});
// Start account switch
act(() => {
result.current.startAccountSwitch();
});
// Verify state preservation
expect(result.current.isSwitchingAccount).toBe(true);
expect(result.current.previousData).toEqual({
hasNFT: true,
tokenId: 1,
backgroundColor: '#FF0000',
keyColor: '#00FF00'
});
});
Integration Testing
Test the store’s interaction with the data fetching layer:
// Mock the blockchain queries
vi.mock('wagmi', () => ({
useReadContract: vi.fn(() => ({
data: [BigInt(42), true],
isLoading: false
}))
}));
// Test the complete flow
test('NFT data updates store correctly', async () => {
const { result } = renderHook(() => useNFTData());
await waitFor(() => {
expect(useNFTStore.getState().tokenId).toBe(42);
expect(useNFTStore.getState().hasNFT).toBe(true);
});
});
Common Patterns and Anti‑Patterns
Recommended Patterns
Use Selective Subscriptions – Subscribe only to the fields your component needs to minimize re-renders.
Coordinate Through Hooks – Let the useNFTData
hook manage store updates rather than updating directly from components.
Handle Loading States – Always set loading states during async operations to provide user feedback.
Anti‑Patterns to Avoid
Direct Blockchain Queries in Components – Use the established data flow through hooks rather than querying contracts directly.
Multiple Rapid Updates – Batch related updates using setTokenData
rather than calling individual setters.
Ignoring Account Switches – Always consider the isSwitchingAccount
state when displaying NFT data to prevent UI flashing and data loss.
Summary
The NFT store represents a carefully designed state‑management solution that balances reactivity, persistence, and performance. Through its sophisticated account‑switching mechanism, selective persistence strategy, and clean API, it provides a robust foundation for NFT‑related features throughout RitoSwap. The store’s integration with blockchain queries through the useNFTData
hook ensures that UI state remains synchronized with on‑chain reality while providing smooth, flicker‑free user experiences during account transitions and data updates.