What building a Reactigotchi can teach you about an applications “memory”

Demystifying state through building digital pets.

What building a Reactigotchi can teach you about an applications “memory”

Remember Tamagotchis? Those tiny digital pets from the 90s that taught an entire generation about responsibility (and the consequences of neglect)? Well they’re back, and not just in the Tamagotchi Paradise I keep on my desk. I decided to recreate that nostalgia in React, building a nerdy alien virtual pet complete with pixel art, stat management, and a mini-game.

Better yet, I did this in <30 minutes (s/o Replit for making me look good), and realized it could be a great beginner project for those new to Replit, React, vibe coding or even“state” as a concept. (Admittedly, that last one used to throw me through a loop).

If you're brand new to React or the concept of state, this post will walk you through the core concepts used to build this app—and explain them in plain English.

If you’ve ever stared at useState() like it’s a cryptic incantation, this post is for you. Let’s break down what’s actually happening—using hungry aliens instead of todo lists.

Pssst — want to just skip to kicking me off of my own high-score board? Play with the Reactigotchi on Replit, or explore the code on GitHub.

ℹ️
For the sake of this blog post and example, we’ll be covering how state is implemented in React — however, the basic concept of state is consistent across languages. 

What even is "state"?

Before diving into the code, let's talk about state. If you're new to programming, state might sound like technical gobbledy-gook, but it's actually something you may encounter nearly every time you surf the web.

State is a concept that you’ll encounter in nearly every programming language, and almost every programming language handles it a little bit differently. 

Put simply, state is your app's memory. 

It’s a snapshot in time of all relevant variables and data structures during your application’s execution.

Think of state like a notebook where your app writes down important information it needs to remember. In my Reactigotchi, the app needs to remember:

  • How hungry is the alien? (a number from 0-100)
  • How happy is it? (another number)
  • How clean is it? (yet another number)
  • Is it currently jumping? (yes or no)
  • Is the mini-game showing? (yes or no)

Every time one of these numbers or yes/no values changes, React automatically redraws the screen to show the new information. That redrawing is called a re-render.

Here's the beautiful part: you don't manually tell React, "hey, redraw the hunger bar now." You just change the number, and React handles the rest. (tada 🪄)

useState: React's memory system

React gives us a tool called `useState` to create the snapshot that is state. Here's how it’s set up in the Reactigotchi's memory:

const [hunger, setHunger] = useState(80);
const [happiness, setHappiness] = useState(80);
const [cleanliness, setCleanliness] = useState(80);
const [isJumping, setIsJumping] = useState(false);
const [showMiniGame, setShowMiniGame] = useState(false);

Let's break down what this code means.

useState(80) says: "React, please remember a number for me. Start it at 80."

React gives you back two things in a box (that's what the [hunger, setHunger] part means):

  1. hunger - the current value (starts at 80)
  2. setHunger - a function to change that value

Think of it like this: hunger is a piece of paper with a number written on it in pencil, and setHunger is a pencil with an eraser that lets you change what's written.

When you call setHunger(50), two things happen:

  1. React updates the number to 50
  2. React automatically re-renders any part of the screen that shows the hunger value

Making stats change: Event Handlers

Now that we have state, how do we change it? Through event handlers—functions that run when something happens (like clicking a button).

Here's the "Feed" button handler in the Reactigotchi application:

const handleFeed = () => {
  setHunger(Math.min(MAX_STAT, hunger + 25));
  setHappiness(Math.min(MAX_STAT, happiness + 5));
};

When you click "Feed," this function runs and:

  1. Increases hunger by 25 (but not above 100, thanks to Math.min)
  2. Also increases happiness by 5 (because eating makes the alien happy!)

The Math.min(MAX_STAT, hunger + 25) part is a safety check.

It says: "Take whichever is smaller: 100 or the new hunger value." This prevents hunger from going above 100.

Here's another handler for cleaning:

const handleClean = () => {
  setCleanliness(MAX_STAT);
  setHasPoop(false);
  setHappiness(Math.min(MAX_STAT, happiness + 10));
};

See how one action (cleaning) changes multiple pieces of state? Cleanliness goes to 100, poop disappears, and happiness gets a boost. These interconnections make the pet feel alive.

Automatic changes: useEffect and stat decay

The trickiest part of a Tamagotchi is that stats decay automatically over time. The alien gets hungrier, sadder, and dirtier — even when you're not clicking anything.

This is where useEffect comes in. It lets you run code on a schedule.

useEffect(() => {
  const decayInterval = setInterval(() => {
    setHunger(prev => Math.max(MIN_STAT, prev - HUNGER_DECAY));
    setHappiness(prev => Math.max(MIN_STAT, prev - HAPPINESS_DECAY));
    setCleanliness(prev => Math.max(MIN_STAT, prev - CLEANLINESS_DECAY));
  }, DECAY_INTERVAL);

  return () => clearInterval(decayInterval);
}, []);

Now, if we’re to put this in layman’s terms — 

"React, every 5 seconds, please reduce hunger by 2, happiness by 1.5, and cleanliness by 1. Keep doing this forever. Oh, and when the component disappears from the screen, stop doing it."

Pay attention to the syntax in this line here: prev => Math.max(MIN_STAT, prev - 2)

Instead of using the current hunger value, we ask React: "What's the most recent hunger value?" This prevents bugs where the value gets stale.

The return () => clearInterval(decayInterval) part is cleanup. When your component unmounts (gets removed from the page), we tell the interval to stop. Without this, the interval would keep running in the background even after the component is gone—a common source of bugs!

The empty [] at the end means "run this effect once when the component first appears, then never again."

Passing state to child components: props

The app is split into multiple components:

  • Tamagotchi.jsx (the parent—holds all the state)
  • AlienCanvas.jsx (draws the pixel alien)
  • GameUI.jsx (shows the stats and buttons)
  • MiniGame.jsx (the book-clicking game)

The parent component (Tamagotchi) holds all the important state. But how does GameUI know what the hunger value is?

Through props—short for "properties." Props are like passing notes between components.

Here's how state is passed to GameUI:

<GameUI
  hunger={hunger}
  happiness={happiness}
  cleanliness={cleanliness}
  onFeed={handleFeed}
  onClean={handleClean}
  onPlay={handlePlay}
/>

Think of this like saying: "Hey GameUI, here's a copy of the current hunger, happiness, and cleanliness numbers. Also, here are some functions you can call to change them."

Inside GameUI.jsx, the component receives these as parameters:

export default function GameUI({ hunger, happiness, cleanliness, onFeed, onClean, onPlay }) {
  return (
    <div className="game-ui">
      {/* ... render hunger bars, buttons, etc ... */}
      <button className="action-btn feed-btn" onClick={onFeed}>
        🍔 Feed
      </button>
    </div>
  );
}

When you click the Feed button, it calls onFeed, which is actually handleFeed from the parent. The parent updates its state, and React automatically sends the new values back down as props. The cycle continues!

This is called unidirectional data flow—data flows down from parent to child, and events flow up from child to parent.

Calculated values: don’t store what you can compute

One clever trick used: checking if the alien is in critical condition.

Instead of creating const [isCritical, setIsCritical] = useState(false) and manually updating it every time a stat changes, we can just calculate it:

const isCritical = hunger < CRITICAL_THRESHOLD || 
                   happiness < CRITICAL_THRESHOLD || 
                   cleanliness < CRITICAL_THRESHOLD;

This value updates automatically whenever any stat changes, because React re-runs this code on every render. No extra state to manage!

If you can calculate something from existing state, just calculate it. Boom!  No syncing required.

It’s like checking the gas light instead of writing down “out of gas” every 10 miles.

CSS: Making things look good

Let’s be honest: I did not build this app purely for the educational value of exploring Replit.

I also wanted to make something like it fell out of a 1997 Best Buy box set.

So while we’ve covered how state makes your app work — let’s now cover how CSS makes it look good. Here’s how we gave it some CSS-powered serotonin. 😎

The Retro Font: Press Start 2P

The entire aesthetic hinges on the font. The app imports Google Fonts in my CSS:

@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');

.tamagotchi-container {
  font-family: 'Press Start 2P', cursive;
}

This pixelated font instantly evokes 80s arcade games. Combined with thick text shadows, it creates that authentic feel:

.title {
  color: #fff;
  font-size: 20px;
  text-shadow: 
    3px 3px 0px #000,
    -1px -1px 0px #000,
    1px -1px 0px #000,
    -1px 1px 0px #000,
    1px 1px 0px #000;
}

Those five text shadows create a black outline effect—pure CSS, no images needed.

CSS animations: free performance, no JavaScript libraries needed.

Many times, we see animations within our applications rely on JavaScript libraries to make things move.  However, this can impact an apps performance.  For this application, we’re using CSS animations. The browser can optimize these to run on the GPU, giving you smooth 60fps motion.

Here's what it looks like for the floating title animation:


@keyframes titleFloat {
  0%, 100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-10px);
  }
}

.title {
  animation: titleFloat 3s ease-in-out infinite;
}
```

This creates a gentle up-and-down bobbing motion. The `infinite` keyword means it loops forever.

The stat bars pulse to show they're "alive":

```css
@keyframes pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.8;
  }
}

.stat-fill {
  animation: pulse 2s ease-in-out infinite;
}
```

In the mini-game, items appear with a spin and scale animation:

```css
@keyframes itemAppear {
  from {
    transform: scale(0) rotate(180deg);
    opacity: 0;
  }
  to {
    transform: scale(1) rotate(0deg);
    opacity: 1;
  }
}

.game-item {
  animation: itemAppear 0.3s ease-out;
}

These animations run on the browser's compositor thread, meaning they're performant even on slower devices.

Gradient backgrounds and layered shadows

The retro aesthetic comes from layering visual effects. Check out the container background:

.tamagotchi-container {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

This creates a purple-to-pink gradient that looks like an old CRT screen.

For depth, the app uses multiple box shadows:

canvas {
  border: 8px solid #0f3460;
  box-shadow: 
    0 0 0 4px #e94560,
    0 10px 30px rgba(0, 0, 0, 0.5),
    inset 0 2px 10px rgba(255, 255, 255, 0.1);
}

Three shadows create a layered effect:

  1. A 4px solid pink outline
  2. A soft drop shadow below for depth
  3. An inset highlight for that screen shine

This makes a flat rectangle look like a physical device.

Dynamic styling: changing colors based on state

The stat bars change color based on their values. Here's the logic:

const getBarColor = (value) => {
  if (value > 60) return '#00ff88';  // Green
  if (value > 30) return '#ffaa00';  // Orange
  return '#ff3366';                   // Red
};

Then it can be applied as an inline style:

<div 
  className="stat-fill" 
  style={{ 
    width: `${hunger}%`,
    backgroundColor: getBarColor(hunger)
  }}
/>

This hybrid approach—CSS classes for structure, inline styles for dynamic values—keeps code maintainable. The bar width and color update reactively as state changes, creating smooth visual feedback.

Component-scoped CSS

Each component has its own CSS file:

  • Tamagotchi.css
  • GameUI.css
  • MiniGame.css
  • AlienCanvas.jsx (uses inline canvas rendering)

This organization keeps styles close to their components. When I'm working on GameUI, I only need to look at GameUI.jsx and GameUI.css. This is much easier to manage than one giant stylesheet. 

Additionally, having everything in one style sheet runs the risk of confusion.  CSS is read from top to bottom, meaning later rules can override earlier rules, causing for conflicts and leav eyou struggling to find what exactly is causing the issue at hand.

Pixel Art with canvas

The alien is drawn pixel-by-pixel on an HTML5 Canvas. To keep pixels crisp when scaled up, use:

<canvas
  style={{ imageRendering: 'pixelated' }}
/>

Inside the canvas, each "pixel" is drawn as an 8×8 square:

const drawPixel = (x, y, color) => {
  ctx.fillStyle = color;
  ctx.fillRect(x * 8, y * 8, 8, 8);
};

This abstraction lets us think in grid coordinates. Drawing the alien's head becomes:


drawPixel(centerX, centerY - 8, bodyColor);
drawPixel(centerX - 1, centerY - 8, bodyColor);
drawPixel(centerX + 1, centerY - 8, bodyColor);

The animation loop uses requestAnimationFrame for smooth motion:

const animate = () => {
  frameRef.current++;
  
  // Blink every 120 frames (~2 seconds at 60fps)
  if (frameRef.current % 120 === 0) {
    isBlinking = true;
  } else if (frameRef.current % 120 === 10) {
    isBlinking = false;
  }
  
  drawAlien(jumpOffset);
  animationRef.current = requestAnimationFrame(animate);
};

This creates a living, breathing alien that blinks and responds to clicks.

Beyond the browser: local vs. server state

Right now, all the alien’s stats live in the browser.  Close the tab and—poof—it’s gone.(Which, honestly, might be merciful if you’ve been ignoring it.)

But what if I wanted persistence? A leaderboard? Saved pets? Cloud-based alien daycares?

That’s where server state enters the picture. It’s the difference between your pet’s temporary mood (local) and its permanent record (server).

Local state and server state are both forms of memory — they just live in very different places:

Type

Lives Where

Survives Refresh?

Example

Local State

Browser Memory (React)

❌ Nope

hunger, isJumping

Server State

Database / API

✅ Yes

high scores, saved pets

Local state is fast, reactive, and personal — perfect for UI updates, quick animations, or temporary values.Server state is slower but persistent — it’s what lets your app “remember” after you rage quit, close the application.

A tale of two states

When you only have local state, your alien lives a very short life. Close the tab? Reset to defaults. When you add server state, your alien starts having continuity.

That’s where the architecture gets interesting.

You now have two brains:

  1. Your alien’s short-term memory (local)
  2. The cloud’s long-term memory (server)

The trick is keeping them in sync — and that’s where most real-world complexity begins.

Fetching data: “Hey server, what’s my score?”

Let’s say you want to pull your high scores from a backend. In React, you’d probably reach for useEffect and fetch:

useEffect(() => {
  const fetchHighScores = async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch('/api/high-scores');
      const data = await response.json();
      setHighScores(data);
    } catch (err) {
      setError('Failed to load high scores');
    } finally {
      setLoading(false);
    }
  };
  
  fetchHighScores();
}, []);

This pattern is common when working with databases:

  1. Set `loading` to true
  2. Try to fetch data
  3. If successful, update state with the data
  4. If it fails, save the error message
  5. Set loading to false

Your UI can then show different things based on these states:

if (loading) return <div>Loading high scores...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{/* Show the scores */}</div>;

This pattern — loading → success → error,  is foundational.It’s how you keep the UI honest about what’s happening behind the scenes.

If your server takes a second to respond, your app shouldn’t panic; it should show a spinner.If the request fails, don’t gaslight the user — tell them the truth (and maybe throw in a funny error message: “Alien leaderboard unavailable. Maybe they unionized.”)

Submitting data: “Add it to the scoreboard please!”

Once you’re fetching, you’ll want to send data too.  When your mini-game ends and you want to submit a score:

const handleGameEnd = async (score) => {
  const happinessBoost = score * 5;
  setHappiness(Math.min(MAX_STAT, happiness + happinessBoost));
  setShowMiniGame(false);
  
  // Submit score to database
  try {
    await fetch('/api/high-scores', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ score, name: 'Player' })
    });
    
    // Refresh the leaderboard
    const response = await fetch('/api/high-scores');
    const data = await response.json();
    setHighScores(data);
  } catch (err) {
    console.error('Failed to submit score:', err);
  }
};

Here’s the interesting part — you’re updating two states:

  • Local (so your UI feels instant)
  • Server (so it doesn’t disappear later)

If the server request fails, you’ve got a decision to make:- Do you roll back the local change?- Do you show an error toast?- Or do you pretend nothing happened and quietly log it (a.k.a. the “likely in some episode of Silicon Valley” approach)?

That mental juggling act — keeping local and server data aligned — is what is called state synchronization.

When things fall out of sync

Now imagine multiple users feeding their aliens at once. Someone else’s alien scores higher, and your leaderboard is outdated.

Suddenly, you’ve got stale data.

This is the moment you realize: State isn’t just a variable — it’s a promise.A promise that what you’re seeing is true right now.

To keep that promise, you’ve got a few strategies:

  • Polling: Re-fetch data every few seconds to stay current
  • WebSockets: Keep a live connection and listen for updates in real time
  • Optimistic updates: Assume success immediately, then roll back if something fails

These tradeoffs define most modern web apps.Every chat bubble, like button, or multiplayer game you’ve ever used is juggling this same balance.

Server State: the mental model

Think of server state like asking a friend a question over text message:

  1. You send the question (request)
  2. You wait for a reply (loading)
  3. They might respond (success) or not (error)
  4. Their answer might be outdated by the time you read it (stale data)

Local state is like your own thoughts—instant and always up-to-date, but only you know them (a dangerous reality in some situations).

Managing both requires thinking about:

  • When to fetch fresh data
  • How to handle loading and errors
  • What to do if requests fail
  • How to keep the UI responsive

This is a big topic! Libraries like React Query, SWR, and Apollo Client exist to make server state management easier. But understanding the fundamentals—useState, useEffect, async/await, loading/error states—is essential.

So if you're learning React, build something ridiculous. A clicker game, a pixel art editor, a digital garden. The constraints of a self-contained project teach you more than any tutorial.

And when your alien develops "brain rot" from neglect (yes, I added spinning rainbow pixels for critically low stats), you'll understand exactly why—and how to fix it.

Big picture: State is everywhere

Local state handles what’s happening right now in your browser.Server state handles what’s true for everyone across sessions and devices.

Understanding the difference — and how they dance together — can help you build better apps and better grasp the fundamentals of web development.

Hopefully, Reactigotchi can help that lesson click in the most literal way possible.

Every time you click “Feed,” your alien will get happier locally— but nothing really counts until you have a high score on the leaderboard too! 

Try this yourself! The full code is available, and it runs entirely in Replit. No local setup required—just hit "Run" and start keeping your nerdy alien alive.

Wanna explore more? Try modifying the decay rates, adding new stats (like "knowledge" that goes up when you play the mini-game), or changing the alien's appearance based on its mood. The beauty of state-driven apps is that one small change ripples through the entire experience.

Happy coding, and don't let your alien get brain rot! 🛸