This is a simple countdown game built with React . The primary focus of this project is to demonstrate the use of useRef
.
The game counts down from a specified time and updates the UI in real-time.
- Real-time countdown timer
- Start, and reset functionality
- Simple and clean user interface
- Focus on the
useRef
hook for managing timer state and modal visibility
- React
- JavaScript (ES6+)
- HTML5
- CSS3
In this project, useRef
is used to manage the timer's state and control the visibility of the result modal.
Unlike useState
, which triggers a re-render when updated, useRef
provides a way to persist values between renders without causing a re-render.
-
useRef
is used to createtimer
anddialog
references for managing the interval timer and modal visibility. -
useImperativeHandle
in theResultModal
component allows the parent component to control the modal's visibility by exposing theopen
method. - The
TimerChallenge
component manages the countdown logic and interacts with theResultModal
component through refs.
Between TimerChallenge
and ResultModal
:
- Parent-Child Relationship:
TimerChallenge
: The parent component that holds the timer logic and manages the state of the challenge.ResultModal
: The child component that displays the result modal.TimerChallenge
controls it using a ref.
import { useState, useRef } from 'react';
import ResultModal from './ResultModal.jsx';
export default function TimerChallenge({ title, targetTime }) {
const dialog = useRef();
if (timeRemaining <= 0) {
clearInterval(timer.current);
dialog.current.open(); // Calls the open method
}
...
}
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { createPortal } from 'react-dom';
const ResultModal = forwardRef(function ResultModal(
{ targetTime, remainingTime, onReset }, ref) {
const dialog = useRef();
...
});
export default ResultModal;
useImperativeHandle
:- This hook is used inside
ResultModal
to expose a customopen
method. - This method is used to show the modal dialog when called.
- This hook is used inside
useImperativeHandle(ref, () => {
return {
// This method is used to show the modal dialog when called.
open() {
dialog.current.showModal(); // Show the modal dialog
},
};
});
createPortal
:- The
createPortal
function is used to render the modal in a different part of the DOM. - This allows the modal to overlay other content and not be constrained by the parent component's styling or structure.
- The
return createPortal(
<dialog ref={dialog} className="result-modal">
...
</dialog>,
document.getElementById('modal') // Attach the modal to the DOM element with id 'modal'
);
});
export default ResultModal;
-
In
TimerChallenge
:- A ref (
dialog
) is created usinguseRef
. - This ref is passed to
ResultModal
using theref
attribute. - When the timer reaches zero,
dialog.current.open()
is called to show the modal.
- A ref (
-
In
ResultModal
:useImperativeHandle
customizes the ref to expose theopen
method.- The
open
method usesdialog.current.showModal()
to display the modal.
- Parent Component:
TimerChallenge
is the parent component because it renders and controlsResultModal
. - Child Component:
ResultModal
is the child component, which usesuseImperativeHandle
to expose itsopen
method to the parent component. - Interaction:
TimerChallenge
can programmatically controlResultModal
through the ref and the exposedopen
method, allowing it to open the modal when needed.
import { useState, useRef } from 'react';
import ResultModal from './ResultModal.jsx';
export default function TimerChallenge({ title, targetTime }) {
const timer = useRef();
const dialog = useRef();
const [timeRemaining, setTimeRemaining] = useState(targetTime * 1000);
const timerIsActive = timeRemaining > 0 && timeRemaining < targetTime * 1000;
if (timeRemaining <= 0) {
clearInterval(timer.current);
dialog.current.open();
}
function handleReset() {
setTimeRemaining(targetTime * 1000);
}
function handleStart() {
timer.current = setInterval(() => {
setTimeRemaining((prevTimeRemaining) => prevTimeRemaining - 10);
}, 10);
}
function handleStop() {
dialog.current.open();
clearInterval(timer.current);
}
return (
<>
<ResultModal
ref={dialog}
targetTime={targetTime}
remainingTime={timeRemaining}
onReset={handleReset}
/>
<section className="challenge">
<h2>{title}</h2>
<p className="challenge-time">
{targetTime} second{targetTime > 1 ? 's' : ''}
</p>
<p>
<button onClick={timerIsActive ? handleStop : handleStart}>
{timerIsActive ? 'Stop' : 'Start'} Challenge
</button>
</p>
<p className={timerIsActive ? 'active' : undefined}>
{timerIsActive ? 'Time is running...' : 'Timer inactive'}
</p>
</section>
</>
);
}
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { createPortal } from 'react-dom';
const ResultModal = forwardRef(function ResultModal(
{ targetTime, remainingTime, onReset },
ref
) {
const dialog = useRef();
const userLost = remainingTime <= 0;
const formattedRemainingTime = (remainingTime / 1000).toFixed(2);
const score = Math.round((1 - remainingTime / (targetTime * 1000)) * 100);
useImperativeHandle(ref, () => ({
open() {
dialog.current.showModal();
},
}));
return createPortal(
<dialog ref={dialog} className="result-modal">
{userLost && <h2>You lost</h2>}
{!userLost && <h2>Your Score: {score}</h2>}
<p>
The target time was <strong>{targetTime} seconds.</strong>
</p>
<p>
You stopped the timer with <strong>{formattedRemainingTime} seconds left.</strong>
</p>
<form method="dialog" onSubmit={onReset}>
<button>Close</button>
</form>
</dialog>,
document.getElementById('modal')
);
});
export default ResultModal;
import { useState, useRef } from 'react';
export default function Player() {
const playerName = useRef();
const [enteredPlayerName, setEnteredPlayerName] = useState(null);
function handleClick() {
setEnteredPlayerName(playerName.current.value);
playerName.current.value = '';
}
return (
<section id="player">
<h2>Welcome {enteredPlayerName ?? 'unknown entity'}</h2>
<p>
<input ref={playerName} type="text" />
<button onClick={handleClick}>Set Name</button>
</p>
</section>
);
}
import Player from './components/Player.jsx';
import TimerChallenge from './components/TimerChallenge.jsx';
function App() {
return (
<>
{/* Render the Player component */}
<Player />
<div id="challenges">
{/* Render multiple TimerChallenge components with different 'target times' */}
<TimerChallenge title="Easy" targetTime={1} />
<TimerChallenge title="Not easy" targetTime={5} />
<TimerChallenge title="Getting tough" targetTime={10} />
<TimerChallenge title="Pros only" targetTime={15} />
</div>
</>
);
}
export default App;
- Clone the repository:
git clone https://github.com/yourusername/react-countdown-game.git
- Navigate to the project directory:
cd react-countdown-game
- Install the dependencies:
npm install
- Start the development server:
npm start
- Open your browser and go to
http://localhost:5173
.
Feel free to dive into the code to understand the implementation details. Happy coding! πππ©βπ»
Copyright Β© Shani Bider, 2024