Junior React Coding Interview Challenge: Interactive Map
Ace your coding interview and Get Hired!
Hey there! Today, we're going to dive into a fun coding challenge in React. I stumbled upon this intriguing challenge on YouTube, thanks to a video by WebDevCody. You can check out the video here
I thought it would be an excellent opportunity to sharpen our skills and share this experience with you. Don't hesitate to drop your feedback in the comments section; I would love to hear from you!
The Challenge Overview
Before we get our hands dirty with the code, let's quickly understand what we're building. Our goal is to create an interactive map represented by a black box. When you click anywhere on the map, a white dot should appear exactly where you clicked. Our app needs to support the following functionalities:
1. Undo: This button allows you to revert the last action, removing the most recently added dot.
2. Redo: With this button, you can redo the last undone action, restoring the dot to the map.
3. Clear: Clicking this button clears all the dots from the map, giving you a clean slate.
You can see a demo of the final result in the video below
Setting Up the App
I've set up a React app using Vite and TailwindCSS, so we can dive straight into the coding challenge. The key to tackling React coding challenges is to break the problem down into smaller, manageable pieces. So, let's begin by adding a div element that will represent our map. We'll give it a black background, make it take up the entire screen, and set it as a flex container with a column direction.
const App = () => {
return <div className="bg-black h-screen w-screen flex flex-col"></div>;
};
Handling Map Clicks
Now, let's make our map interactive. We want to be able to click on it and place a dot at the exact position of the click. To achieve this, we'll add an onClick event listener to the map div and retrieve the click's position using the event object. Let's create a function called handleMapClick to handle this event. It will receive a React.MouseEvent object as an argument.
const App = () => {
const handleMapClick = (event: React.MouseEvent<HTMLDivElement>) => {
console.log(event.clientX, event.clientY);
};
return <div className="bg-black h-screen w-screen flex flex-col"></div>;
};
The clientX and clientY properties of the event object represent the position of the click relative to the top-left corner of the screen.
Storing Clicked Dots
Now, we can store the position of the click in a state variable. Let's call it dots, and we'll initialize it with an empty array. We'll also define a type for the dots. Each dot will have top and left properties, both of which will be numbers.
type Dot = {
top: number;
left: number;
};
const App = () => {
const [dots, setDots] = useState<Dot[]>([]);
const handleMapClick = (event: React.MouseEvent<HTMLDivElement>) => {
const { clientY, clientX } = event;
const newDot: Dot = {
top: clientY,
left: clientX,
};
setDots([...dots, newDot]);
};
return <div className="bg-black h-screen w-screen flex flex-col"></div>;
};
Now, every time we click on the map, we'll add a new dot to the dots array.
Rendering Dots on the Map
To visualize the dots on the map, we can map over the dots array and render a div for each dot. Since we don't have a unique identifier for each dot, we can use the index of the dot in the array as a key.
const App = () => {
return (
<div className="bg-black h-screen w-screen flex flex-col">
{dots.map(({ top, left }, index) => (
<div
key={index}
className="w-2 h-2 bg-white rounded-full absolute transform -translate-x-1/2 -translate-y-1/2"
style={{ top, left }}
></div>
))}
</div>
);
};
Important Note: In an interview, your interviewer might ask you why using the index as key is not problematic in this case. The reason is that we are not changing the order of the dots array, we are only adding new dots to the end of the array. If we were to change the order of the dots array, then using the index as key would be problematic.
This will render the dots on the map, but they won't be in the exact position of the click because we haven't accounted for the dot's size yet. To fix this, we can use the transform property in CSS. We'll set the translateX and translateY properties to -50%, effectively moving the dot to the left and top by half of its width and height.
...
<div className="bg-black h-screen w-screen flex flex-col">
{dots.map(({ top, left }, index) => (
<div
key={index}
className="w-2 h-2 bg-white rounded-full absolute transform -translate-x-1/2 -translate-y-1/2"
style={{ top, left }}
></div>
))}
</div>
Now, the dots will appear exactly where you click on the map.
Adding Undo Functionality
Next, we want to enable users to undo the last action. We'll add an Undo button to the top of the screen with appropriate styles. Let's also define a function, handleUndo, that will handle the click event.
const App = () => {
const handleUndo = () => {
const lastDot = dots.pop();
if (!lastDot) return;
setDots([...dots]);
};
return (
<div className="bg-black h-screen w-screen flex flex-col">
<button className="bg-white text-black p-2 m-2" onClick=. {handleUndo}>
Undo
</button>
{/* ... */}
</div>
);
};
In the handleUndo function, we use the pop method to remove the last element from the dots array and store it in lastDot. We then update the state to reflect the changes.
Adding Redo Functionality
Now, let's implement the Redo functionality. Similar to the Undo button, we'll add a Redo button and define a function, handleRedo, to handle the click event.
const App = () => {
const handleRedo = () => {
const lastRemovedDot = removedDots.pop();
if (!lastRemovedDot) return;
setDots([...dots, lastRemovedDot]);
};
return (
<div className="bg-black h-screen w-screen flex flex-col">
<button className="bg-white text-black p-2 m-2" onClick={handleUndo}>
Undo
</button>
<button className="bg-white text-black p-2 m-2" onClick={handleRedo}>
Redo
</button>
{/* ... */}
</div>
);
};
To make this work, we'll need to keep track of removed dots as well. Let's add a state variable for removedDots and initialize it with an empty array.
const [removedDots, setRemovedDots] = useState<Dot[]>([]);
In the handleUndo function, we'll now store the removed dot in the removedDots array before updating the dots state.
const handleUndo = () => {
const lastDot = dots.pop();
if (!lastDot) return;
setRemovedDots([...removedDots, lastDot]);
setDots([...dots]);
};
Adding Clear Functionality
Finally, let's implement the Clear functionality. Just like before, we'll add a Clear button and define a function, handleClear, to handle the click event.
const App = () => {
const handleClear = () => {
setDots([]);
setRemovedDots([]);
};
return (
<div className="bg-black h-screen w-screen flex flex-col">
<button className="bg-white text-black p-2 m-2" onClick={handleUndo}>
Undo
</button>
<button className="bg-white text-black p-2 m-2" onClick={handleRedo}>
Redo
</button>
<button className="bg-white text-black p-2 m-2" onClick={handleClear}>
Clear
</button>
{/* ... */}
</div>
);
};
In the handleClear function, we reset both the dots and removedDots arrays to empty arrays, effectively clearing all the dots from the map.
Refactoring for Clarity
To write clean and readable code, let's make a few improvements:
1. Create a Button component for better code organization and reusability.
2. Extract the rendering of dots into a separate Dot component.
Here's the refactored code:
const Button = ({ children, ...props }: HtmlAttributes<HtmlbuttonElement>) => {
return <button {...props} className="bg-white text-black p-2 m-2" />;
};
const Dot = ({ top, left }: Dot) => {
return (
<div
className="w-2 h-2 bg-white rounded-full absolute transform -translate-x-1/2 -translate-y-1/2"
style={{ top, left }}
/>
);
};
const App = () => {
// ...
return (
<div className="bg-black h-screen w-screen flex flex-col">
<Button onClick={handleUndo}>Undo</Button>
<Button onClick={handleRedo}>Redo</Button>
<Button onClick={handleClear}>Clear</Button>
{dots.map(({ top, left }, index) => (
<Dot key={index} top={top} left={left} />
))}
</div>
);
};
With these improvements, our app is not only fully functional but also cleaner and more organized. You can now add dots, undo actions, redo actions, and clear the map with ease.
Congratulations on completing this coding challenge! I hope you enjoyed it and learned something valuable. If you have any questions or feedback, please don't hesitate to let me know in the comments. Thanks for joining me, and I'll see you in the next coding challenge!