Introduction
So, for a month, during lunch breaks, I was bored at the client’s office. And yes, since I was fasting for Ramadan, I had to keep myself busy at the office while the others were enjoying themselves at the restaurant. I was procrastinating when I had this random thought - what if Tic-Tac-Toe pieces disappeared after a while? Would make for a more chaotic game, right? I spent a weekend hacking together a prototype, and honestly, it turned out way better than I expected. Friends kept asking how I built it, so I figured I’d write up my process. Here’s how I created a disappearing Tic-Tac-Toe game using React Native and Expo, warts and all.
Tech Stack
I didn’t overthink the tech stack too much - just went with what I know and like:
- React Native & Expo: Yeah, I know some hardcore devs scoff at Expo, but I needed to ship quickly and didn’t want to mess with native modules.
- Expo Router: File-based routing that actually makes sense - been using this in all my projects lately.
- Zustand: God, I used to be such a Redux fanboy until I tried Zustand. Now I can’t go back. So much less boilerplate!
- React Native Reanimated: The regular Animation API gave me headaches with the fading effects. Reanimated just works.
- Lucide Icons: Nice icons, simple API, what else do you need?
- Supabase: I was this close to just using Firebase again, but decided to give Supabase a shot. Glad I did.
Game Mechanics and State Management
First attempt at the state structure was a mess - I kept losing track of which moves were supposed to disappear when. After a few refactors and a late night with too much coffee, I settled on this:
interface GameState {
board: Board;
currentPlayer: Player;
timeouts: { index: number; timestamp: number }[];
fadeTimeSeconds: number;
winner: Player | null;
isDarkTheme: boolean;
moves: number;
}
The real breakthrough was tracking each move with its own timestamp. Seems obvious in hindsight, but took me embarrassingly long to figure out.
The Disappearing Moves System
This part gave me the most trouble. I tried at least three different approaches before landing on one that felt right:
Tracking Timeouts: Each move gets logged with exactly when it happened:
const newTimeouts = [...timeouts, { index, timestamp: Date.now() }];Cleanup of Expired Moves: I set up an interval that runs every second to check if any moves should vanish:
checkTimeouts: () => { const currentTime = Date.now(); const newTimeouts = timeouts.filter(timeout => { const elapsed = (currentTime - timeout.timestamp) / 1000; if (elapsed >= fadeTimeSeconds) { board[timeout.index] = null; // Clear the move return false; } return true; }); }Smooth Animations: The first version looked terrible - pieces would just abruptly disappear. Adding a fade-out animation made all the difference:
const opacity = useDerivedValue(() => { const timeout = timeouts.find(t => t.index === index); if (!timeout) return 1; const elapsedTime = (Date.now() - timeout.timestamp) / 1000; return interpolate( elapsedTime, [0, fadeTimeSeconds], [1, 0], 'clamp' ); });
My girlfiend (my primary tester lol) said watching the X’s and O’s slowly fade was oddly satisfying. That’s when I knew I was onto something.
Feedback System and Modal Integration
After showing an early version to some friends, I got bombarded with feature requests and bug reports via WhatsApp. Quickly realized I needed a proper way to collect feedback:
Feedback Modal Component
Nothing fancy - just a simple modal with a form. Backend is a dead-simple Supabase table:
CREATE TABLE feedback (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
type text NOT NULL CHECK (type IN ('bug', 'feedback')),
content text NOT NULL,
created_at timestamptz DEFAULT now(),
status text DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'resolved')),
user_email text
);
Frontend state is pretty minimal:
const [type, setType] = useState<FeedbackType>('feedback');
const [content, setContent] = useState('');
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'sending' | 'success' | 'error'>('idle');
Made email optional but encouraged - helped me track down a weird bug that only happened on certain Android devices.
UI/UX Design Considerations
Dark/Light Theme
Can’t believe I almost shipped without a dark mode! My eyes burn just thinking about it. Ended up with:
- Dark Mode: Went with
#1a1a1abackground and#2a2a2afor secondary elements. Not pitch black because that causes eye strain (learned that the hard way on previous projects). - Light Mode: Clean
#fffbackground with#f0f0f0secondary elements. Nothing groundbreaking here.
I’m still not 100% happy with the transitions between themes, but it’s good enough for v1.
Cross-Platform Considerations
Ugh, the bane of my existence. Web vs mobile differences kept cropping up:
padding: Platform.OS === 'web' ? 40 : 20,
fontSize: Platform.OS === 'web' ? 64 : 48,
My filesystem is littered with screenshots of how things looked across devices. iPad was particularly annoying to get right - not quite mobile, not quite desktop.
Performance Optimizations
Animations and Rendering
Thought everything was fine until my mom tried it on her ancient phone and it ran at like 2 FPS:
- Had to migrate all animations to React Native Reanimated’s worklet functions to run off the JS thread
- Found some rookie mistakes in my code causing massive re-renders (classic me)
- Had to sacrifice some fancier effects on older devices using Platform.OS checks
Worth the effort though - now the game runs smoothly even on my mom’s phone, and she’s actually my most active player!
Conclusion
What started as a way to avoid client work turned into a pretty fun side project. The disappearing move mechanic adds just enough chaos to make Tic-Tac-Toe interesting again. I’ve got a Trello board full of ideas for v2 - online multiplayer, different board sizes, and custom fade times are top of the list.
PS: For those asking about downloading the app - I’ve submitted it to the Google Play Store and it’s currently in the review process (fingers crossed it doesn’t get rejected for being too addictive!). The iOS version will come after the Android release is stable, so Apple users will need to be patient. The App Store review process is a whole other beast I’m not quite ready to tackle yet. I’ll update this post once it’s available for download!