Roomba

React Hard 60 minShopify

Prompt

The goal is to create a Roomba board (like the vacuum), where you'll have 2 buttons: Turn Right and Move Forward. Your board will be an 6 x 6 matrix. You'll have a pointer on the board that moves as you press move forward and points in whatever direction you're going. Don't forget to add styling to the board and the buttons.

  • Each click on Move Forward will move the Roomba one block
  • When hitting an edge (where there's no more space to go), turn right and keep moving as you click
  • Each click on Turn Right will rotate the Roomba 90 degrees to the right while staying in the same position

If you can complete the above requirements in less than 1 hour, try to add the following:

  • Create some random barriers on the board. When hitting a barrier, turn right
  • Allow controlling the pointer using the arrow keys on your keyboard
  • Display the number of movements

Hi there! Just so you know, these bonus/extras/step ups are typically included in interviews for candidates who finish the main challenge early. They help interviewers gather more information about your skills and determine if you might be a good fit for a higher-level role. Happy coding!

Example

Playground

Solution

Explanation

Let's break down how our solution works, exploring component structure, state management, and the logic behind our virtual vacuum cleaner.

Component Structure

I've organized the code into several focused components.

  1. App.js: The main container that manages our application state and behavior
  2. Grid.js: Responsible for rendering the board matrix
  3. GridItem.js: Represents each cell in our grid
  4. Roomba.js: Renders our directional pointer which is the Roomba vaccum cleaner
  5. constants.js: Contains configuration constants and mapping objects

This component structure follows the single responsibility principle, with each component handling a specific concern. It makes our code more maintainable and easier to understand.

State Management

In App.js, we manage two key pieces of state:

const [direction, setDirection] = useState('right');
const [position, setPosition] = useState({ x: 0, y: 0 });
  1. direction: Tracks which way the Roomba is facing ('up', 'right', 'down', or 'left')
  2. position: Stores the current coordinates of the Roomba as an object with x and y properties

These two pieces of state are sufficient to determine everything about our Roomba's current status - where it is and which direction it's facing.

Core Logic

The heart of our application is in two main functions: handleTurnRight and handleMoveForward.

Turning the Roomba

const handleTurnRight = useCallback(() => {
setDirection(nextDirection[direction]);
}, [direction]);

The handleTurnRight function is beautifully simple. Instead of using a series of if/else statements, we use a lookup object from our constants file. This makes our direction changes clear and concise - when the Roomba is facing 'right' and turns right, it will now face 'down', and so on in a clockwise pattern.

export const nextDirection = {
right: 'down',
down: 'left',
left: 'up',
up: 'right',
};

Moving the Roomba

The handleMoveForward function has a bit more logic we are doing few things here.

  1. Takes the current position and calculates a new position based on the direction
  2. Checks if the new position would be outside the grid boundaries
  3. If it would go out of bounds, the Roomba turns right instead of moving
  4. If it's a valid move, updates the position state
const handleMoveForward = useCallback(() => {
let { x, y } = position;

if (direction === 'right') x++;
if (direction === 'down') y++;
if (direction === 'left') x--;
if (direction === 'up') y--;

if (
x < 0 ||
x > matrixSize - 1 ||
y < 0 ||
y > matrixSize - 1
) {
handleTurnRight();
} else {
setPosition({ x, y });
}
}, [direction, position, handleTurnRight]);

Using useCallback for both functions ensures they don't get recreated on every render, which is a small but meaningful optimization.

Rendering the Grid

The Grid component is responsible for creating our 6x6 matrix. We are passing 6 as the number of rows and columns to the Grid component. However, if we want to change the size of the grid, we can easily do so by changing the matrixSize constant. This shows how reusable our component is.

const Grid = ({
numOfRows,
numOfColumns,
position,
direction,
}) => {
const matrix = useMemo(
() =>
Array(numOfRows).fill(
new Array(numOfColumns).fill(false)
),
[numOfRows, numOfColumns]
);

return (
<div className="grid">
{matrix.map((col, colIndex) => {
return (
<div className="row" key={colIndex}>
{col.map((row, rowIndex) => {
const isRoomba =
colIndex === position.y &&
rowIndex === position.x;

return (
<GridItem
isRoomba={isRoomba}
direction={direction}
key={`${colIndex}-${rowIndex}`}
/>
);
})}
</div>
);
})}
</div>
);
};

For each cell, we determine if it contains the Roomba by comparing its coordinates with the current position. If isRoomba is true, we want to display the Roomba component in that cell. Which we do in the Roomba component.

Displaying the Roomba

The Roomba component uses emojis to represent the vacuum's direction:

const Roomba = ({ direction }) => {
if (direction === 'up') return <Emoji symbol="👆" />;
if (direction === 'down') return <Emoji symbol="👇" />;
if (direction === 'right') return <Emoji symbol="👉" />;
if (direction === 'left') return <Emoji symbol="👈" />;

return null;
};

Using emojis makes our Roomba instantly recognizable and adds a fun element to the UI. The directional hand emojis naturally indicate which way the Roomba is facing.

Styling

The CSS is clean and straightforward:

.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px;
}

.grid {
display: flex;
flex-direction: column;
gap: 2px;
}

.grid-item {
border: 1px solid black;
width: 32px;
height: 32px;
display: inline-block;
overflow: hidden;
margin: 2px;
text-align: center;
}

We use flexbox to center our content and create a clean layout. Each grid cell is a fixed size with a border to create our board visualization.

Putting It All Together

When a user interacts with our application:

  • Clicking "Turn Right" calls handleTurnRight, which updates the direction state
  • Clicking "Move Forward" calls handleMoveForward, which either:
    • Updates the position state to move the Roomba forward
    • Turns the Roomba right if it would hit an edge

The Grid component renders the board based on the current position and direction, and the Roomba component displays the appropriate directional emoji.

Roomba Bonus

We are able to solve the Roomba initial requirements in 30 minutes and now we have 30 minutes left to solve the bonus requirements, so let's do that. Playground given below is the solution of the initial requirements. You can use it as a reference to solve the bonus requirements.

Roomba Bonus Solution

Bonus Explanation

Movements Counter

We will start by adding a new state variable to our App component which will keep track of the number of movements.

const [movements, setMovements] = useState(0);

We will update the handleMoveForward function to increment the movements state variable only when the Roomba moves forward.

setMovements(movements + 1);

Just above the grid, we will add a new div which will display the number of movements. We will give same actions class to it so it can be styled the same way as the buttons and give same margin bottom.

<div className="actions">
<p>Movements: {movements}</p>
</div>

Keyboard Controls

useEffect(() => {
const handleKeyDown = (event) => {
const pressedDirection = arrowDirection[event.key];
if (pressedDirection === direction) {
handleMoveForward();
} else {
setDirection(pressedDirection);
}
};

document.addEventListener('keydown', handleKeyDown);

return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [direction, position]);

The useEffect hook is setting up an event listener for keyboard inputs that allows users to control the Roomba using arrow keys. Here's what it's doing step by step:

  • We define a handleKeyDown function that will be called whenever a key is pressed
  • When a key is pressed, we check if it's one of our arrow keys by looking it up in the arrowDirection object
  • The arrowDirection object maps keyboard keys to directions:
    • ArrowRight: 'right'
    • ArrowDown: 'down'
    • ArrowLeft: 'left'
    • ArrowUp: 'up'
  • If the pressed key's direction matches the Roomba's current direction, we call handleMoveForward() to move the Roomba forward
  • If the pressed key's direction is different, we update the Roomba's direction using setDirection(pressedDirection)
  • We add the event listener when the component mounts using document.addEventListener('keydown', handleKeyDown)
  • We remove the event listener when the component unmounts or when dependencies change, using the cleanup function returned from useEffect
  • The dependencies array [direction, position] ensures our effect runs again whenever the direction or position changes, so our event handler always has access to the latest state values

This approach gives users two ways to control the Roomba:

  • If they press an arrow key that matches the current direction, the Roomba moves forward
  • If they press an arrow key for a different direction, the Roomba turns to face that direction

The cleanup function is crucial for preventing memory leaks - it removes the event listener when the component unmounts or before the effect runs again with new dependencies.

Adding Barriers

1. Generating the Barriers

First, we create a function in our constants file that generates random barrier positions:

export const generateBarriers = (numBarriers = 5) => {
const barriers = [];

while (barriers.length < numBarriers) {
const x = Math.floor(Math.random() * matrixSize);
const y = Math.floor(Math.random() * matrixSize);

// Don't place barrier at starting position (0,0)
if (!(x === 0 && y === 0)) {
const barrierKey = `${x},${y}`;
if (!barriers.includes(barrierKey)) {
barriers.push(barrierKey);
}
}
}

return barriers;
};

This function is like setting up furniture in a room:

  • It creates 5 random barriers by default
  • Each barrier gets a random x,y position on our grid
  • We make sure not to put any barriers at (0,0) - that's where our Roomba starts!
  • We store barrier positions as strings like "2,3" to make them easy to check later

2. Using the Barriers in Our App

In our App component, we use useMemo to generate the barriers once when the app starts:

const barriers = useMemo(() => generateBarriers(), []);

We use useMemo because:

  • We only want to generate barriers once when the game starts
  • The barriers should stay in the same place throughout the game
  • We don't want to recalculate barrier positions on every render

3. Checking for Barriers During Movement

In our handleMoveForward function, we check if the Roomba would hit a barrier:

if (
x < 0 ||
x > matrixSize - 1 ||
y < 0 ||
y > matrixSize - 1 ||
barriers.includes(`${x},${y}`)
) {
handleTurnRight();
} else {
setPosition({ x, y });
setMovements(movements + 1);
}

Just like with walls, if our Roomba would hit a barrier:

  • It doesn't move to that position
  • Instead, it turns right to try a different direction

4. Displaying the Barriers

In our Grid component, we check each cell to see if it contains a barrier:

const isBarrier = barriers.includes(
`${rowIndex},${colIndex}`
);

return (
<GridItem
isRoomba={isRoomba}
isBarrier={isBarrier}
direction={direction}
key={`${colIndex}-${rowIndex}`}
/>
);

And in our GridItem component, we show a barrier emoji if needed:

const GridItem = ({ isRoomba, isBarrier, direction }) => {
return (
<div className="grid-item">
{isRoomba && <Roomba direction={direction} />}
{isBarrier && (
<span role="img" aria-label="barrier">
🚧
</span>
)}
</div>
);
};

This is how we add a barrier emoji to our grid.

Interviewer Criteria

HTML/CSS

  • Does my layout accurately match the provided image?

  • Did you style the UI quickly (10 minutes or less)?

  • Are my CSS class names both self-explanatory and meaningful?

JavaScript

  • Have I leveraged ES6 features efficiently, such as let, const, arrow functions, and destructuring?

  • Are my variable and function names descriptive, maintaining a consistent naming convention?

  • Was I swiftly able to create the logic for the Roomba to move and edge detection?

React

  • Are props passed down appropriately between components?

  • Is there clear separation of concerns between components?

  • Have I used the key prop appropriately on all iterated elements, ensuring efficient and correct re-rendering?

  • Do I know how event listeners work in React? Was I able to use it to control the Roomba?

  • Was I able to cleanup the event listener when the component unmounts?

  • I made sure that I not storing barries in the state because they are not changing.

  • Did I used useMemo to generate barriers only once?

Component Architecture

  • Did I create separate components for <Grid />, <GridItem />, and <Roomba />?

  • Are the names of my classes, functions, handlers, and components clear and understandable?

Time Checkpoints

  • 10:00 AM

    Interview starts 👥

  • 10:03 AM

    Candidate reads prompt and asks clarifying questions

  • 10:12 AM

    Generate grid from matrixSize and add action buttons

  • 10:30 AM

    Turn right and move forward functionality implemented

  • 10:36 AM

    Basic styling completed

  • 10:38 AM

    Movements counter implemented

  • 10:45 AM

    Keyboard controls implemented

  • 10:55 AM

    Barriers implemented

  • 11:00 AM

    Interview ends ✅

00:00