React Unpacked: A Working Introduction
React has been around since roughly v0.12 in 2014, and while it has evolved considerably, the underlying mental model has remained consistent. The shift from directly manipulating the DOM to thinking in terms of components and state is the core adjustment for new developers. The most effective way to internalize this is to build something tangible. In this guide, we’ll cover the foundational concepts—JSX, rendering, components, and props—so you can start creating your own applications or jump straight into building a game from scratch.
The Anatomy of a First App
Booting up a React application can be done in several ways, but the essence is straightforward. At its most basic, you need a bit of HTML and a script to inject your code. Here is a complete, minimal example:
import React from 'https://cdn.skypack.dev/react'
import { render } from 'https://cdn.skypack.dev/react-dom'
const App = () => <h1>{`Time: ${Date.now()}`}</h1>
render(<App/>, document.getElementById('app')
This snippet shows a component named App being passed to React DOM for rendering. However, React can also render an HTML element directly, which results in a slightly more compact version:
render(<h1>{`Time: ${Date.now()}`}</h1>, document.getElementById('app'))
The important distinction here is that App is a component—a function that returns elements. The HTML tags (<h1>, <span>) are the elements themselves. This leads to the first big “aha” moment: components produce elements.
Understanding JSX Syntax
That “HTML in JS” you see is called JSX. It is a syntax extension for JavaScript that lets you write markup directly within your logic. It functions as a templating language with the full power of JavaScript at its disposal, but it is ultimately an abstraction over a different API. While you can use JSX without React, in practice it is the primary way you will structure your UI and bind event handlers. Occasionally, you may need to render outside this structure using a Portal, but for most development, JSX is your tool.
There are some syntax rules to remember. Attributes are written in camelCase; for instance, onclick becomes onClick. There are also exceptions like class, which becomes className, and the style attribute, which accepts an object rather than a string.
A quick comparison shows how JSX translates to raw function calls:
React.createElement('h1', null, `Time: ${Date.now()}`)
This level of transformation is a good reminder that JSX is just an abstraction. While you won't typically write the underlying API calls, understanding it clarifies how React manages your markup. Elements such as a simple button with a class are easy to construct:
<div className="awesome-class" style={{ color: 'red' }}>Cool</div>
How React Puts Things on the Page
To get your JSX into the DOM, you use React DOM to inject it at a single point of entry. Once this happens, everything within that point is managed by React. This is where the Virtual DOM comes in. Direct DOM manipulation causes the browser to recalculate rendering changes, which can be expensive. React mitigates this by performing updates in memory and then syncing them with the browser’s DOM in batches, which offers performance benefits when your UI changes often.
It is important to note that React does not have to manage an entire page. You can have multiple independent React apps on a single page, or just a single section managed by it. Consider this example where the same app is rendered twice, sandwiched between regular HTML. It displays the current time using Date.now:
const App = () => <h1>{`Time: ${Date.now()}`}</h1>
Thinking in Components and Props
This is arguably the most important concept to grasp. Components are reusable blocks of UI, but underneath the surface, they are just functions. The arguments passed to these functions are called props (short for properties). Props are read-only and are used to dictate what a component renders. You can pass anything as a prop, including other components or data. To access content placed between a component’s tags, you use a special prop called children. A component must always return something—either a set of elements or null if you want to render nothing.
You can write components in very different ways that yield the same result. The classic approach uses a function:
function App() {
return <h1>{`Time: ${Date.now()}`}</h1>
}
An alternative, class-based syntax was the standard before Hooks were introduced. Class-based components were necessary for managing state and accessing the component lifecycle, but with Hooks, function-based components have become the modern standard. They require less code, are simpler to read, and avoid the confusion around this binding and context found in classes.
You will often see components written with a concise arrow function, which returns implicitly. Even in our earlier example, we used a different style. As long as your function returns what you want to render, it is valid:
const App = () => <h1>{`Time: ${Date.now()}`}</h1>
Let’s enhance our multi-app example by extracting logic into reusable components. Instead of hardcoding a static message, we can pass a message prop:
const Message = ({ message }) => <h1>{message}</h1>
const App = ({ message }) => <Message message={message} />
render(<App message={`Time: ${Date.now()}`}/>, document.getElementById('app'))
Now, by changing the message prop passed to App, we can render different text without duplicating code. This decision-making process—knowing what to extract into a component—is the core architectural skill in React. It is like playing with LEGO blocks for user interfaces.
What happens if a prop isn’t provided? You can give it a default value in your function signature. This is often cleaner than using the defaultProps property on the function itself:
const Message = ({message = "You forgot me!"}) => <h1>{message}</h1>
You can also specify alternatives using defaultProps on your component, or even better, add propTypes to establish a type-checking system for your props. Props are just an object, so you can access them in any way you like. While you can destructure them directly in the function parameters, they are equally accessible on the props object itself:
const Message = (props) => <h1>{props.message}</h1>
Similarly, you can use “prop spreading” to pass through several properties at once, though it is generally better to be explicit about what you pass. You also have the option to pass content as a child, which is then accessed via the special children prop:
const Message = ({ children }) => <h1>{children}</h1>
const App = ({ message }) => <Message>{message}</Message>
Since anything can be a prop, you can even pass a component as a prop to another component. This is a less common pattern, but it demonstrates the system’s flexibility:
const Time = ({ children }) => <h1>{`Time: ${children}`}</h1>
const App = ({ message, messageRenderer: Renderer }) => <Renderer>{message}</Renderer>
render(<App message={`${Date.now()}`} messageRenderer={Time} />, document.getElementById('app'))
Notice how the messageRenderer prop has to be renamed to Renderer in the destructuring. Starting with a capital letter tells React to treat an identifier as a component to render, not an HTML element like <messageRenderer>. Remember, passing a prop is only the first step; the receiving component must also process it, such as applying any passed-in styles to its own elements.
Regardless of how you structure your code, the core design philosophy is building small, composable pieces. A theoretical layout might look like this:
const Layout = ({ children }) => (
<div className="layout">
<Header/>
<main>{children}</main>
<Footer/>
</div>
)
Finally, a tool you should investigate early on is Storybook. It helps cultivate this component-driven development mindset, which is extremely valuable across all modern frontend frameworks. Start experimenting; practicing how to break a layout into functional components is a skill you will hone with every project. For more details on the underlying ecosystem, the global React documentation is the perfect companion to this practical introduction.
State, Effects, and Refs
Static rendering only gets us so far. In React, components can hold state, and state changes drive re-renders. When a piece of state updates, React re-renders only the parts of the UI that depend on that state. The data flow is unidirectional: state flows down the component tree via props, and updates to that state can only be triggered from the top through events or callbacks passed down as props.
A simple timer demonstrates this. The component below updates its displayed time every second by leveraging React hooks: useState to hold the current time, useEffect to set up and tear down the setInterval, and useRef to keep a mutable reference to the interval ID.
import React, { useEffect, useRef, useState } from 'https://cdn.skypack.dev/react'
import { render } from 'https://cdn.skypack.dev/react-dom'
const Time = () => {
const [time, setTime] = useState(Date.now())
const timer = useRef(null)
useEffect(() => {
timer.current = setInterval(() => setTime(Date.now()), 1000)
return () => clearInterval(timer.current)
}, [])
return <h1>{`Time: ${time}`}</h1>
}
const App = () => <Time/>
render(<App/>, document.getElementById('app'))
Notice how we never assign to the state variable directly. useState returns a state variable and a function to update it; the variable is treated as immutable. The useEffect hook accepts a dependency array as its second argument — the effect re-runs only when items in that array change. An empty array means the effect runs only on mount; omitting the array runs the effect on every render. The function returned from the effect runs on unmount, which is where you clean up like clearing the interval.
A ref is useful when you need a reference to something that doesn't affect rendering, such as a timer ID. In the example, the interval is cleared on unmount so there are no memory leaks.
The timer works because we trigger a state update inside the interval. Each update causes the component to re-render and display the new time. If we want the component to be controllable from above, we lift the interval duration into a parent component's state and pass it down.
Here, the App component owns the interval state, which is used to configure the Time child. Changing the interval value triggers the effect inside Time to run again, because the dependency array now lists interval. The new interval is applied to the timer logic.
const App = () => {
const [interval, updateInterval] = useState(1000)
return (
<Fragment>
<Time interval={interval} />
<h2>{`Interval: ${interval}`}</h2>
<input type="range" min="1" value={interval} max="10000" onChange={e => updateInterval(e.target.value)}/>
</Fragment>
)
}
The Fragment component is used here because a component must return a single child (or null). Wrapping output in a div is not always desirable; fragments let you group elements without injecting extra markup. This example also shows the first event binding: an onChange handler on an input updates the interval state.
const Time = ({ interval }) => {
const [time, setTime] = useState(Date.now())
const timer = useRef(null)
useEffect(() => {
timer.current = setInterval(() => setTime(Date.now()), interval)
return () => clearInterval(timer.current)
}, [interval])
return <h1>{`Time: ${time}`}</h1>
}
Try adjusting the interval in the demo to see the effect re-run.
Game Components and Design
Now we can build something more complete. The game requires these pieces:
- Start/stop controls
- A countdown timer
- A score counter
- A layout container
- Mole components that respond to clicks
The core UI is a Game component that holds most of the shared state.
import React, { Fragment } from 'https://cdn.skypack.dev/react'
import { render } from 'https://cdn.skypack.dev/react-dom'
const Moles = ({ children }) => <div>{children}</div>
const Mole = () => <button>Mole</button>
const Timer = () => <div>Time: 00:00</div>
const Score = () => <div>Score: 0</div>
const Game = () => (
<Fragment>
<h1>Whac-A-Mole</h1>
<button>Start/Stop</button>
<Score/>
<Timer/>
<Moles>
<Mole/>
<Mole/>
<Mole/>
<Mole/>
<Mole/>
</Moles>
</Fragment>
)
render(<Game/>, document.getElementById('app'))
A playing boolean state variable is the master switch. Conditional rendering with && shows the board only while playing is true, and a ternary controls whether the button reads "Start" or "Stop".
const Game = () => {
const [playing, setPlaying] = useState(false)
return (
<Fragment>
{!playing && <h1>Whac-A-Mole</h1>}
<button onClick={() => setPlaying(!playing)}>
{playing ? 'Stop' : 'Start'}
</button>
{playing && (
<Fragment>
<Score />
<Timer />
<Moles>
<Mole />
<Mole />
<Mole />
<Mole />
<Mole />
</Moles>
</Fragment>
)}
</Fragment>
)
}
The timer's duration is a constant defined outside the component tree, which is a good habit for any configurable value.
const TIME_LIMIT = 30000
The Timer component needs three things: the countdown value, the update interval, and a callback for when time runs out.
const Timer = ({ time, interval = 1000, onEnd }) => {
const [internalTime, setInternalTime] = useState(time)
const timerRef = useRef(time)
useEffect(() => {
if (internalTime === 0 && onEnd) onEnd()
}, [internalTime, onEnd])
useEffect(() => {
timerRef.current = setInterval(
() => setInternalTime(internalTime - interval),
interval
)
return () => {
clearInterval(timerRef.current)
}
}, [])
return <span>{`Time: ${internalTime}`}</span>
}
A naive timer implementation fails because the interval callback closes over stale state. With an empty dependency array, the effect runs once, capturing the initial value of internalTime. The fix is to update the effect's dependencies, but that leads to drift with shorter intervals — the longer the timer runs or the smaller the interval, the more inaccurate it gets. A better approach is to track the internal time with a ref, which updates without triggering re-renders.
useEffect(() => {
timerRef.current = setInterval(
() => setInternalTime(internalTime - interval),
interval
)
return () => {
clearInterval(timerRef.current)
}
}, [internalTime, interval])
const timeRef = useRef(time)
useEffect(() => {
timerRef.current = setInterval(
() => setInternalTime((timeRef.current -= interval)),
interval
)
return () => {
clearInterval(timerRef.current)
}
}, [interval])
Rendering the time as seconds is easy: divide by 1000 and append a literal s to the string.
See the Pen [4. Rudimentary Timer](https://codepen.io/smashingmag/pen/oNZXEVp) by @jh3y.
This timer will drift slightly over long periods, but for this game it is sufficient.
Scoring and Re-rendering Behaviour
Scoring happens when a mole is clicked. Each mole gets a 200 point value, passed down via an onWhack callback. Clicking a mole updates score state.
const MOLE_SCORE = 100
const Mole = ({ onWhack }) => (
<button onClick={() => onWhack(MOLE_SCORE)}>Mole</button>
)
const Score = ({ value }) => <div>{`Score: ${value}`}</div>
const Game = () => {
const [playing, setPlaying] = useState(false)
const [score, setScore] = useState(0)
const onWhack = points => setScore(score + points)
return (
<Fragment>
{!playing && <h1>Whac-A-Mole</h1>}
<button onClick={() => setPlaying(!playing)}>{playing ? 'Stop' : 'Start'}</button>
{playing &&
<Fragment>
<Score value={score} />
<Timer
time={TIME_LIMIT}
onEnd={() => setPlaying(false)}
/>
<Moles>
<Mole onWhack={onWhack} />
<Mole onWhack={onWhack} />
<Mole onWhack={onWhack} />
<Mole onWhack={onWhack} />
<Mole onWhack={onWhack} />
</Moles>
</Fragment>
}
</Fragment>
)
}
The React Developer Tools browser extension can highlight renders in real time. When checking the debug demo, the timer updates as time changes, but every click on a mole re-renders all components. That is an important performance observation for a larger app.
Rendering moles individually is verbose. Instead, loops are the idiomatic way to render a collection in JSX — most of the time that means Array.map.
An alternative is to build the collection with a for loop and call the function from inside JSX. The key attribute is critical in any list — React uses it to decide which DOM nodes to keep, move, or delete. Always use a stable unique identifier when one is available; falling back to an index is memory-intensive and can be reordered by React.
const USERS = [
{ id: 1, name: 'Sally' },
{ id: 2, name: 'Jack' },
]
const App = () => (
<ul>
{USERS.map(({ id, name }) => <li key={id}>{name}</li>)}
</ul>
)
return (
<ul>{getLoopContent(DATA)}</ul>
)
new Array(NUMBER_OF_THINGS).fill().map()
The final mole grid uses staggered timing so each mole independently appears and disappears.
return (
<Fragment>
<h1>Whac-A-Mole</h1>
<button onClick={() => setPlaying(!playing)}>{playing ? 'Stop' : 'Start'}</button>
{playing &&
<Board>
<Score value={score} />
<Timer time={TIME_LIMIT} onEnd={() => console.info('Ended')}/>
{new Array(5).fill().map((_, id) =>
<Mole key={id} onWhack={onWhack} />
)}
</Board>
}
</Fragment>
)
Practice hooks and components long enough, and conditional rendering, effects, and list loops will feel like second nature.
Game Over And Restart States
Ending the game currently relies entirely on the Start button, and the score resets abruptly on restart. The Timer's onEnd callback is also unused. A cleaner approach is to introduce a third UI state — a finished state — separate from the initial "fresh" state and the active playing state.
Rather than toggling a single boolean, we can split the logic into two functions: startGame and endGame. Starting a game resets the score and sets playing to true. Ending it sets finished to true, leaving the score intact so it can be displayed as the final result.
When the timer runs out, it should trigger this same endGame path.
The Timer component can handle this inside an effect: when its internal countdown reaches 0, it unmounts and invokes onEnd.
The main game render can then branch on these states:
- Fresh (initial view)
- Playing (active game with timer)
- Finished (final score shown, option to restart)
This also reveals a chance to reduce repetition: the Score component appears in multiple states. Whether to hoist it into its own conditional or use a more generic wrapper depends on your design preference, but it's worth considering for separation of concerns and component portability.
Building Moles With Refs And Animation
Moles are the self-contained centerpiece of the interaction. They shouldn't depend on the surrounding app logic; they simply report a score via an onWhack prop. This keeps them portable.
For presentation, we want moles to pop up from and disappear into a container with overflow: hidden. The default position of the Mole button should be hidden out of view.
We'll use GreenSock (GSAP) to handle the bobbing animation. Third-party DOM animation libraries fit naturally with React's refs and effects pattern: grab the element with useRef, then run the animation inside useEffect. It's important to use a ref rather than a className query here, because a class name could match multiple elements and cause the animation to target all of them.
The Mole component wraps the button in a container for show/hide control and attaches a ref to the button for the GSAP tween that moves it up and down.
Several refinements improve the gameplay loop:
- Each mole should animate at a different speed.
- The points awarded for whacking should decrease the longer a mole stays active.
- Props like
speed,delay, andpointsmake each mole instance configurable.
Since the points decay over time and don't require re-renders, a ref is the right home for that value — it survives closures without causing unnecessary renders. A separate ref keeps a handle to the GSAP animation so it can be stopped or modified when the mole is whacked. The cleanup function returned from the effect kills the animation on unmount, preventing stray repeat timers from firing later.
When a mole is clicked, a whack function sets a whacked state to true and invokes onWhack with the current points value from the ref. An effect watches the whacked state:
- If true, it resets the points.
- Pauses the animation and moves the mole underground.
- Waits a random delay, then restarts the animation at a faster
timescale. - Resets
whackedback tofalse.
Props for the moles must not be generated inline during render — that would create new values on every render and cause issues. Instead, generate a fresh array of mole configurations when the game starts and iterate over that array. This keeps randomness without breaking React's render cycle.
The result is a fully functional whac-a-mole game in under 200 lines of React code, with room to style and extend.
Persisting High Scores With A Custom Hook
Tracking the highest score across sessions is a perfect use case for a custom hook. We can write a usePersistentState wrapper around useState that reads and writes to localStorage automatically.
The hook can be used in the game component exactly like useState, and the onWhack callback can update the high score whenever the current score surpasses it. Determining whether a new high score was just achieved can be handled with additional state, allowing for a UI cue during the game.
Adding Audio With Another Custom Hook
Audio effects add a playful layer. A rudimentary custom audio hook takes a src and returns an API to play it. The question is where to invoke it: inside the Mole, passed down as a prop, or called in the onWhack handler in the parent game component.
These decisions are central to component-driven development. Keeping portability in mind, you might prefer to control audio at the Game level, especially if you later want global mute functionality. If you have many sounds, naming the play methods explicitly can get tedious — returning an array from the hook like useState lets you alias each method, though it makes remembering positional values harder.
Where To Take It From Here
This walkthrough covered the essentials you'll use constantly in React projects:
- Scaffolding an app
- JSX syntax
- Components and props
- Building timers
- Working with refs
- Creating custom hooks
The game is a solid foundation for experimentation. You could style it further, add new mechanics, or refactor parts to be more generic. For deeper learning, the official React documentation remains the best starting point, along with well-known articles on making setInterval declarative with hooks, fetching data, and understanding when to reach for useMemo and useCallback.



