Day 3 - A 'recently used' component for VannaCharm's Symbol Overview page

Usability improvement: a React component that tracks and displays recently viewed symbols using local storage

Written by Chris on December 3rd, 2025

This post is part of the 'The 12 Days of Full Stack Dev' series, an adventure through full stack fintech app development. Join me through the first 12 days of December as we develop a variety of new features, fixes, and components within the Full Stack Craft fintech family.

For the full list of days and topics in this series, see the original post: The 12 Days of Full Stack Dev.

Leveraging Local Storage for Recently Used Items

Today, I needed a bit of a smaller "scooby snack" task after yesterday's IV surface and smoothing task - that was a big one!

The concept of this React component is rather simple - we will use a comma separated list in local storage to keep track of recently visited symbols in our 'Symbol Overview' page on VannaCharm. When an item is used, we add it to the front of the list, ensuring no duplicates and maintaining a maximum length. More specifically:

  • The maximum number of items to store should be no more than 10
  • The most recent ticker should be at the front of the list
  • No duplicates should be allowed - if an item is already in the list, it should be moved to the front

We can encapsulate this logic fairly elegantly in a custom React hook:

import { useEffect, useState } from 'react';

const RECENTLY_USED_KEY = 'VANNACHARM_RECENTLY_USED_SYMBOLS';

export const useRecentlyUsedSymbols = () => {
    const [recentlyUsedSymbols, setRecentlyUsedSymbols] = useState<string[]>(() => {
        const stored = localStorage.getItem(RECENTLY_USED_KEY);
        return stored ? stored.split(',') : [];
    });

    const addSymbolToFrontUniquely = (symbol: string) => {
        // regardless if symbol is in or not, we can filter it out first
        let newList = recentlyUsedSymbols.filter((s) => s !== symbol);

        // then ensure it is at the front
        newList.unshift(symbol);
        
        // limit to 10 items
        if (newList.length > 10) {
            newList = newList.slice(0, 10);
        }
        
        // update both state and local storage with the new list
        setRecentlyUsedSymbols(newList);
        localStorage.setItem(RECENTLY_USED_KEY, newList.join(','));
    };

    // on mount, get the current symbol from the URL
    useEffect(() => {
        const params = new URLSearchParams(window.location.search);
        const symbol = params.get('symbol');
        if (symbol) {
            addSymbolToFrontUniquely(symbol);
        }
    }, []);

    return recentlyUsedSymbols
}

Render It!

With our hook complete, we just add a few link chips that are derived directly from the recently used symbols list:

import { useRecentlyUsedSymbols } from '@/hooks/useRecentlyUsedSymbols';

export function RecentlyUsedSymbols() {
    const recentlyUsedSymbols = useRecentlyUsedSymbols();

    return (
        <div>
            <span className="text-sm text-gray-400 mr-2">Recently Viewed:</span>
            <div className="flex flex-wrap gap-2 items-center mt-2">
                {recentlyUsedSymbols.map((symbol) => (
                    <a
                        key={symbol}
                        href={`/symbol-overview?symbol=${symbol}`}
                        className="px-3 py-1 rounded text-xs font-medium transition-colors border border-white text-white hover:bg-white hover:text-black"
                    >
                        {symbol}
                    </a>
                ))}
            </div>
        </div>
    );
}

Here we are using tailwind classes, but of course any styling approach will work.

Thanks Again!

Thanks for stopping by for day 3! Happy holidays!

-Chris

More posts:

footer-frame