MongoDB Peer-Programming: Building Features Live

The project you'll build on

MongoDB sends you a real, public starter project ahead of time, drawn from its public mongodb-js GitHub repositories. The exact repo can vary from loop to loop, so we'll rehearse on one of their real example projects, the mongodb-js/electron-template: you download it, run it, and during the interview you build features on top of it live.

Know the template cold

The template is intentionally tiny. That's good news. The whole point is that you understand it in minutes and start adding features, not that you decode a huge app. Below is the exact starter repo in our explorer, every file, byte-for-byte from main. Click around the tree:

electron-template: the exact starter project

Explorer

App.tsxsrc/App.tsx
1import React, { useState } from 'react';
2 
3import Button from './components/button/Button';
4import { sum } from './modules/sum';
5 
6import './styles.css';
7 
8export const App: React.FunctionComponent = ({}) => {
9 const [ count, setCount ] = useState(0);
10 
11 return (
12 <div>
13 <h1 className="title">Title</h1>
14 <div>
15 Count: {count}
16 </div>
17 <div>
18 <Button
19 className="add-one-button"
20 onClick={() => setCount(count => sum(count, 1))}
21 >Add one</Button>
22 </div>
23 </div>
24 );
25}
26 
27export default App;

What each file is doing

It looks like a lot of files, but only a handful carry meaning. Group them in your head like this:

The Electron main process (the desktop shell):

  • src/main.ts, the Node entry point. It creates a BrowserWindow, loads index.html into it, and opens the DevTools. The nodeIntegration: true / contextIsolation: false settings are permissive for a starter template, a great thing to flag out loud as "not how I'd ship a hardened app," but fine here.
  • src/index.html, the shell page Electron loads. The only line that matters is <div id="app"></div>. That's where React mounts.

The renderer (your React app):

  • src/renderer.tsx, the browser-side entry point. It grabs #app and calls createRoot(...).render(<App />). This is the bridge from "a blank HTML div" to "a running React tree."
  • src/App.tsx, the only UI file, and where you'll spend ~all your time. Out of the box it's a counter: local useState, a title, and one button.
  • src/components/button/Button.tsx, a small, reusable presentational component. It owns no state; it just takes children, className, and onClick. This is the template showing you the pattern it wants you to follow when you add components.
  • src/modules/sum.ts, a pure helper (sum(a, b)), deliberately kept out of the components so it's trivial to unit-test. Another pattern hint: pure logic lives in modules/.
  • src/styles.css, global CSS, imported straight into App.tsx.

Build & config (you rarely touch these, but know what they do):

  • package.json, scripts are the thing to read: npm run start cleans dist, runs webpack, then runs webpack in watch mode and launches Electron together (via concurrently). npm run test runs Jest.
  • webpack.config.js exports two configs, webpack.electron.js (target electron-main, builds main.tsdist/main.js) and webpack.react.js (target electron-renderer, builds renderer.tsxdist/renderer.js, and injects it into index.html). Two bundles, one for each process.
  • tsconfig.json, babel.config.js, TypeScript + Babel/JSX setup. jsx: "react-jsx" is why you don't import React just to use JSX.

Tests (Jest + React Testing Library):

  • test/App.spec.tsx renders <App /> and asserts the title is visible. test/modules/sum.spec.ts unit-tests sum. test/setup.ts wires up @testing-library/jest-dom. jest.config.js maps .ts/.tsx through ts-jest, .js through babel-jest, and stubs CSS imports with identity-obj-proxy.

The one-sentence mental model

If they say "walk us through it," say this and you'll sound organized:

The three files that explain almost the whole runtime are src/main.ts (creates the window), src/renderer.tsx (mounts React), and src/App.tsx (the UI). Know those cold; skim the rest.

What's deliberately missing (and likely to be asked)

The template has no list rendering, forms, search, async loading, routing, or custom hooks. So the feature prompts you get are almost always "add one of those." The sequence below is built around exactly those gaps.

How the round actually flows

They hand you small, escalating feature prompts, each one building on the last. You implement, narrate as you go, and they help if you get stuck. The bar for a senior hire is less "React wizardry" and more: structured problem-solving, sensible state decisions, clean incremental changes, and clear communication.

We believe the exact prompt varies from session to session, so don't expect the wording below word-for-word. The shape of the round, though, is consistent: you take a small app and grow a list-and-form UI one feature at a time. So rather than guess the precise prompt, we'll work through a representative sequence that exercises every skill this round tends to probe. For each task: the prompt, a quick plan, the code (in the explorer, with changed lines highlighted and modified files dotted in the tree), and what the interviewer is really checking.

Our running feature: a Collection Manager

The textbook version of this exercise is a humble to-do list, and we believe a close cousin of it is what tends to show up here: an item tracker, a contacts list, a notes panel. The theme shifts from candidate to candidate; the underlying React does not. Whatever the surface, the thing being measured is the same: can you manage state across a list and a form, with a few small components?

So we'll build the version MongoDB engineers would actually recognise, a tiny Collection Manager, the kind of thing MongoDB Compass does. We'll manage the collections in a database:

  • create a collection (with a name),
  • drop one,
  • search collections by name,
  • rename a collection,
  • mark which collections are indexed, and
  • filter by index status.

Every one of those is a thin MongoDB-flavoured wrapper around a core React skill the round likes to test. If your real prompt turns out to be a to-do app or an address book instead, the moves are identical; you're just renaming things. Let's go.


Task 1: Turn the counter into a Collection Manager

Prompt: Replace the counter with a small UI: a text input and a Create collection button that appends the typed name to a list shown on the page. Clear the input after creating, and ignore empty input.

Plan (say it out loud first): "I'll keep the collections in useState as a string array, add a controlled input for the draft name, validate that it's non-empty on create, and render the list with .map. I'll keep it all in App.tsx for now and refactor once it grows."

Task 1: create + list collections

Explorer

App.tsxmodifiedsrc/App.tsx
1import React, { useState } from 'react';
2 
3import Button from './components/button/Button';
4 
5import './styles.css';
6 
7export const App: React.FunctionComponent = () => {
8 const [collections, setCollections] = useState<string[]>([]);
9 const [newName, setNewName] = useState('');
10 
11 function handleCreateCollection() {
12 const trimmed = newName.trim();
13 if (trimmed === '') {
14 return; // ignore empty / whitespace-only names
15 }
16 // Functional update: the next list is derived from the previous one.
17 setCollections((current) => [...current, trimmed]);
18 setNewName('');
19 }
20 
21 return (
22 <div>
23 <h1 className="title">Collection Manager</h1>
24 
25 <input
26 placeholder="New collection name"
27 value={newName}
28 onChange={(event) => setNewName(event.target.value)}
29 />
30 <Button
31 className="create-collection-button"
32 onClick={handleCreateCollection}
33 >
34 Create collection
35 </Button>
36 
37 <ul>
38 {collections.map((name, index) => (
39 <li key={index}>{name}</li>
40 ))}
41 </ul>
42 </div>
43 );
44};
45 
46export default App;

Two small details that signal seniority:

  • Trim then validate. A common bug is checking newName === '' before trimming, then " " (spaces) sneaks in as an empty collection. Trim first, validate second.
  • Functional state update. setCollections((current) => [...current, trimmed]) derives the next list from the previous one, the safe default whenever new state depends on old state. (Direct setCollections([...collections, trimmed]) is usually fine for a single update too, a good thing to mention you know the difference.)

Task 2: Drop a collection

Prompt: Add a Drop button next to each collection that removes it from the list.

Plan: "To remove a specific row safely I need a stable identity per collection, not the array index, indexes shift when items are removed. I'll upgrade each collection from a bare string to an object with a unique id and a name, generate ids with a useRef counter, and filter by id on drop."

This is the moment to introduce a tiny data model. Narrate why: "Index-based keys and index-based deletes get buggy the instant the list reorders or filters. A stable id makes every later feature, search, rename, indexes, straightforward."

Task 2: give each collection an id, then drop by id

Explorer

App.tsxmodifiedsrc/App.tsx
1import React, { useRef, useState } from 'react';
2 
3import Button from './components/button/Button';
4 
5import './styles.css';
6 
7type Collection = {
8 id: number;
9 name: string;
10};
11 
12export const App: React.FunctionComponent = () => {
13 const [collections, setCollections] = useState<Collection[]>([]);
14 const [newName, setNewName] = useState('');
15 const nextId = useRef(0);
16 
17 function handleCreateCollection() {
18 const trimmed = newName.trim();
19 if (trimmed === '') {
20 return;
21 }
22 const collection: Collection = { id: nextId.current++, name: trimmed };
23 setCollections((current) => [...current, collection]);
24 setNewName('');
25 }
26 
27 function handleDropCollection(id: number) {
28 setCollections((current) => current.filter((collection) => collection.id !== id));
29 }
30 
31 return (
32 <div>
33 <h1 className="title">Collection Manager</h1>
34 
35 <input
36 placeholder="New collection name"
37 value={newName}
38 onChange={(event) => setNewName(event.target.value)}
39 />
40 <Button
41 className="create-collection-button"
42 onClick={handleCreateCollection}
43 >
44 Create collection
45 </Button>
46 
47 <ul>
48 {collections.map((collection) => (
49 <li key={collection.id}>
50 <span>{collection.name}</span>
51 <Button
52 className="drop-collection-button"
53 onClick={() => handleDropCollection(collection.id)}
54 >
55 Drop
56 </Button>
57 </li>
58 ))}
59 </ul>
60 </div>
61 );
62};
63 
64export default App;

What changed and why it matters:

  • type Collection = { id; name }. Promoting the model from string to an object is the single highest-leverage decision in the whole session, every feature after this leans on it.
  • useRef for ids. A useRef counter gives monotonically increasing ids without triggering re-renders (it's not UI state). Mention you could use crypto.randomUUID() instead, both are fine; useRef keeps it dependency-free and deterministic.
  • Filter, don't splice. current.filter((c) => c.id !== id) returns a new array and never mutates state.
  • key={collection.id}. Now that rows have stable ids, the React key becomes stable too, no more index keys.

Task 3: Search collections by name

Prompt: Add a search box. As the user types, show only the collections whose name contains the query (case-insensitive). An empty query shows everything.

Plan: "The key decision: I will not store the filtered list in state. I'll store only the search text, and compute the visible list during render from my two sources of truth, collections and searchText. That way the filtered view can never drift out of sync when I add or drop a collection."

This is the most important idea in the whole round, so say it explicitly: derived state.

Task 3: derive the visible list, never store it

Explorer

App.tsxmodifiedsrc/App.tsx
1import React, { useRef, useState } from 'react';
2 
3import Button from './components/button/Button';
4 
5import './styles.css';
6 
7type Collection = {
8 id: number;
9 name: string;
10};
11 
12export const App: React.FunctionComponent = () => {
13 const [collections, setCollections] = useState<Collection[]>([]);
14 const [newName, setNewName] = useState('');
15 const [searchText, setSearchText] = useState('');
16 const nextId = useRef(0);
17 
18 function handleCreateCollection() {
19 const trimmed = newName.trim();
20 if (trimmed === '') {
21 return;
22 }
23 const collection: Collection = { id: nextId.current++, name: trimmed };
24 setCollections((current) => [...current, collection]);
25 setNewName('');
26 }
27 
28 function handleDropCollection(id: number) {
29 setCollections((current) => current.filter((collection) => collection.id !== id));
30 }
31 
32 // Derived state: never stored in useState, always computed during render
33 // from the source-of-truth state (collections + searchText).
34 const normalizedSearch = searchText.trim().toLowerCase();
35 const visibleCollections =
36 normalizedSearch === ''
37 ? collections
38 : collections.filter((collection) =>
39 collection.name.toLowerCase().includes(normalizedSearch)
40 );
41 
42 return (
43 <div>
44 <h1 className="title">Collection Manager</h1>
45 
46 <input
47 placeholder="New collection name"
48 value={newName}
49 onChange={(event) => setNewName(event.target.value)}
50 />
51 <Button
52 className="create-collection-button"
53 onClick={handleCreateCollection}
54 >
55 Create collection
56 </Button>
57 
58 <input
59 placeholder="Search collections"
60 value={searchText}
61 onChange={(event) => setSearchText(event.target.value)}
62 />
63 
64 <ul>
65 {visibleCollections.map((collection) => (
66 <li key={collection.id}>
67 <span>{collection.name}</span>
68 <Button
69 className="drop-collection-button"
70 onClick={() => handleDropCollection(collection.id)}
71 >
72 Drop
73 </Button>
74 </li>
75 ))}
76 </ul>
77 </div>
78 );
79};
80 
81export default App;

Why this is the senior answer:

  • One source of truth. collections is the data; searchText is the query. The visible list is a pure function of those two, so it's recomputed every render and is always correct. If you instead kept visibleCollections in useState, you'd have to remember to re-filter it on every create, drop, and rename, and you'd eventually forget one. That bug is exactly what they're probing for.
  • Normalize once. Compute searchText.trim().toLowerCase() a single time, then compare against each lowercased name. Don't re-lowercase inside the loop more than you need.
  • Empty query short-circuits. When the query is empty, return collections as-is, no needless filtering, and the user sees everything.

Task 4: Handle the empty states

Prompt: When there are no collections at all, show a friendly message instead of a blank area. And when a search matches nothing, say so.

Plan: "Two distinct empty states, and they're not the same thing. 'No collections exist yet' is a first-run state; 'your search matched nothing' is a filtered state. I'll branch on collections.length first, then on visibleCollections.length."

Task 4: distinguish 'nothing yet' from 'nothing matches'

Explorer

App.tsxmodifiedsrc/App.tsx
1import React, { useRef, useState } from 'react';
2 
3import Button from './components/button/Button';
4 
5import './styles.css';
6 
7type Collection = {
8 id: number;
9 name: string;
10};
11 
12export const App: React.FunctionComponent = () => {
13 const [collections, setCollections] = useState<Collection[]>([]);
14 const [newName, setNewName] = useState('');
15 const [searchText, setSearchText] = useState('');
16 const nextId = useRef(0);
17 
18 function handleCreateCollection() {
19 const trimmed = newName.trim();
20 if (trimmed === '') {
21 return;
22 }
23 const collection: Collection = { id: nextId.current++, name: trimmed };
24 setCollections((current) => [...current, collection]);
25 setNewName('');
26 }
27 
28 function handleDropCollection(id: number) {
29 setCollections((current) => current.filter((collection) => collection.id !== id));
30 }
31 
32 const normalizedSearch = searchText.trim().toLowerCase();
33 const visibleCollections =
34 normalizedSearch === ''
35 ? collections
36 : collections.filter((collection) =>
37 collection.name.toLowerCase().includes(normalizedSearch)
38 );
39 
40 return (
41 <div>
42 <h1 className="title">Collection Manager</h1>
43 
44 <input
45 placeholder="New collection name"
46 value={newName}
47 onChange={(event) => setNewName(event.target.value)}
48 />
49 <Button
50 className="create-collection-button"
51 onClick={handleCreateCollection}
52 >
53 Create collection
54 </Button>
55 
56 <input
57 placeholder="Search collections"
58 value={searchText}
59 onChange={(event) => setSearchText(event.target.value)}
60 />
61 
62 {collections.length === 0 ? (
63 <p className="empty-state">No collections yet. Create your first one above.</p>
64 ) : visibleCollections.length === 0 ? (
65 <p className="empty-state">No collections match your search.</p>
66 ) : (
67 <ul>
68 {visibleCollections.map((collection) => (
69 <li key={collection.id}>
70 <span>{collection.name}</span>
71 <Button
72 className="drop-collection-button"
73 onClick={() => handleDropCollection(collection.id)}
74 >
75 Drop
76 </Button>
77 </li>
78 ))}
79 </ul>
80 )}
81 </div>
82 );
83};
84 
85export default App;

Why this is more than a nicety: mixing the two empty states is a real bug. If you only check visibleCollections.length === 0, then a user who has 20 collections but typed a non-matching search sees "No collections yet. Create your first one above", which is confusing and wrong. Branching on the raw collections first, then the filtered list, keeps each message truthful.


Task 5: Rename a collection, and split the UI into components

Prompt: Let the user rename a collection inline, click Rename, edit the name in a text field, then Save or Cancel. Also, App.tsx is getting big; tidy it up.

This is two moves at once, so narrate them separately.

Plan, part 1, the rename interaction: "I'll track which row is being edited with an editingId, plus the in-progress editingName. A row renders either its normal view or an edit field depending on whether its id matches editingId. Save trims, updates that one collection immutably with .map, and clears edit mode."

Plan, part 2, the refactor: "I'll lift the Collection type into its own file, and split the list into a CollectionList (maps the rows) and a CollectionRow (one row, knows how to render view-vs-edit). App stays the single source of truth for state and passes data + callbacks down as props."

Task 5: extract CollectionList + CollectionRow, add inline rename

Explorer

App.tsxmodifiedsrc/App.tsx
1import React, { useRef, useState } from 'react';
2 
3import Button from './components/button/Button';
4import CollectionList from './components/collection-list/CollectionList';
5import { Collection } from './types/Collection';
6 
7import './styles.css';
8 
9export const App: React.FunctionComponent = () => {
10 const [collections, setCollections] = useState<Collection[]>([]);
11 const [newName, setNewName] = useState('');
12 const [searchText, setSearchText] = useState('');
13 const [editingId, setEditingId] = useState<number | null>(null);
14 const [editingName, setEditingName] = useState('');
15 const nextId = useRef(0);
16 
17 function handleCreateCollection() {
18 const trimmed = newName.trim();
19 if (trimmed === '') {
20 return;
21 }
22 const collection: Collection = { id: nextId.current++, name: trimmed };
23 setCollections((current) => [...current, collection]);
24 setNewName('');
25 }
26 
27 function handleDropCollection(id: number) {
28 setCollections((current) => current.filter((collection) => collection.id !== id));
29 }
30 
31 function handleStartRename(collection: Collection) {
32 setEditingId(collection.id);
33 setEditingName(collection.name);
34 }
35 
36 function handleSaveRename() {
37 const trimmed = editingName.trim();
38 if (trimmed === '') {
39 return;
40 }
41 setCollections((current) =>
42 current.map((collection) =>
43 collection.id === editingId ? { ...collection, name: trimmed } : collection
44 )
45 );
46 setEditingId(null);
47 setEditingName('');
48 }
49 
50 function handleCancelRename() {
51 setEditingId(null);
52 setEditingName('');
53 }
54 
55 const normalizedSearch = searchText.trim().toLowerCase();
56 const visibleCollections =
57 normalizedSearch === ''
58 ? collections
59 : collections.filter((collection) =>
60 collection.name.toLowerCase().includes(normalizedSearch)
61 );
62 
63 return (
64 <div>
65 <h1 className="title">Collection Manager</h1>
66 
67 <input
68 placeholder="New collection name"
69 value={newName}
70 onChange={(event) => setNewName(event.target.value)}
71 />
72 <Button
73 className="create-collection-button"
74 onClick={handleCreateCollection}
75 >
76 Create collection
77 </Button>
78 
79 <input
80 placeholder="Search collections"
81 value={searchText}
82 onChange={(event) => setSearchText(event.target.value)}
83 />
84 
85 {collections.length === 0 ? (
86 <p className="empty-state">No collections yet. Create your first one above.</p>
87 ) : visibleCollections.length === 0 ? (
88 <p className="empty-state">No collections match your search.</p>
89 ) : (
90 <CollectionList
91 collections={visibleCollections}
92 editingId={editingId}
93 editingName={editingName}
94 onEditingNameChange={setEditingName}
95 onStartRename={handleStartRename}
96 onSaveRename={handleSaveRename}
97 onCancelRename={handleCancelRename}
98 onDrop={handleDropCollection}
99 />
100 )}
101 </div>
102 );
103};
104 
105export default App;

The senior signals here:

  • State stays lifted. The edit state (editingId, editingName) lives in App, not inside CollectionRow. The rows are controlled. They receive isEditing and call callbacks up. That keeps one source of truth and makes "only one row edits at a time" fall out for free.
  • Components are presentational. CollectionList and CollectionRow own no business state; they render props and forward events. That's the same pattern the template's own Button demonstrates. You're following the codebase's conventions, which interviewers love.
  • A type file. src/types/Collection.ts is now imported by App, CollectionList, and CollectionRow, one definition, no drift.
  • Immutable rename. .map(c => c.id === editingId ? { ...c, name: trimmed } : c) updates one item without mutating the array or the object.

Task 6: Mark which collections are indexed

Prompt: Each collection can be indexed or not. Show that status, and let the user toggle it with an Add index / Drop index button.

Plan: "This is a new piece of data, so it belongs on the model. I'll add indexed: boolean to the Collection type and default it to false on create. Then a toggle handler flips it immutably, and the row shows a badge plus a contextual button label."

Task 6: add `indexed` to the model and surface it in the row

Explorer

CollectionRow.tsxmodifiedsrc/components/collection-row/CollectionRow.tsx
1import React from 'react';
2 
3import Button from '../button/Button';
4import { Collection } from '../../types/Collection';
5 
6type CollectionRowProps = {
7 collection: Collection;
8 isEditing: boolean;
9 editingName: string;
10 onEditingNameChange: (name: string) => void;
11 onStartRename: (collection: Collection) => void;
12 onSaveRename: () => void;
13 onCancelRename: () => void;
14 onToggleIndex: (id: number) => void;
15 onDrop: (id: number) => void;
16};
17 
18export const CollectionRow: React.FunctionComponent<CollectionRowProps> = ({
19 collection,
20 isEditing,
21 editingName,
22 onEditingNameChange,
23 onStartRename,
24 onSaveRename,
25 onCancelRename,
26 onToggleIndex,
27 onDrop,
28}) => {
29 if (isEditing) {
30 return (
31 <li>
32 <input
33 className="rename-input"
34 value={editingName}
35 onChange={(event) => onEditingNameChange(event.target.value)}
36 />
37 <Button className="save-rename-button" onClick={onSaveRename}>
38 Save
39 </Button>
40 <Button className="cancel-rename-button" onClick={onCancelRename}>
41 Cancel
42 </Button>
43 </li>
44 );
45 }
46 
47 return (
48 <li>
49 <span>{collection.name}</span>
50 {collection.indexed && <span className="badge-indexed">indexed</span>}
51 <Button
52 className="toggle-index-button"
53 onClick={() => onToggleIndex(collection.id)}
54 >
55 {collection.indexed ? 'Drop index' : 'Add index'}
56 </Button>
57 <Button className="rename-button" onClick={() => onStartRename(collection)}>
58 Rename
59 </Button>
60 <Button className="drop-collection-button" onClick={() => onDrop(collection.id)}>
61 Drop
62 </Button>
63 </li>
64 );
65};
66 
67export default CollectionRow;

Notice how many files move for one feature, and that the dots in the tree make it obvious: the type changes, App gains a handler, and the prop threads through CollectionList into CollectionRow. That's worth a sentence out loud:

A touch of CSS makes the badge read well, the kind of small polish you'd add in src/styles.css:

.badge-indexed {
  margin: 0 8px;
  padding: 2px 8px;
  border-radius: 10px;
  font-size: 12px;
  color: #00684a;            /* MongoDB forest green */
  background-color: #c3f4d7; /* light spring-green */
}

Task 7: Filter by index status (and unit-test the logic)

Prompt: Add All / Indexed / Not indexed filter buttons that combine with the existing search. While you're at it, can you make the filtering logic testable?

This is the climax, and it ties the room together. Pull the filtering out of the component into a pure function, then write a Jest test for it, mirroring exactly what the template already does with sum.ts + sum.spec.ts.

Plan: "I'll add an indexFilter state, a small union type 'all' | 'indexed' | 'not-indexed'. Then I'll move all filtering into a pure filterCollections(collections, { search, indexFilter }) in modules/, so the component just calls it. Pure logic in modules/ is trivial to unit-test, so I'll add a spec that covers search, index filtering, and the two combined."

Task 7: extract a pure filter module + its Jest spec

Explorer

filterCollections.tsnewsrc/modules/filterCollections.ts
1import { Collection } from '../types/Collection';
2 
3export type IndexFilter = 'all' | 'indexed' | 'not-indexed';
4 
5export type CollectionFilters = {
6 search: string;
7 indexFilter: IndexFilter;
8};
9 
10// Pure function: same inputs always produce the same output, no React,
11// no side effects. That is exactly what makes it trivial to unit-test.
12export function filterCollections(
13 collections: Collection[],
14 filters: CollectionFilters
15): Collection[] {
16 const normalizedSearch = filters.search.trim().toLowerCase();
17 
18 return collections.filter((collection) => {
19 const matchesSearch =
20 normalizedSearch === '' ||
21 collection.name.toLowerCase().includes(normalizedSearch);
22 
23 const matchesIndex =
24 filters.indexFilter === 'all' ||
25 (filters.indexFilter === 'indexed' && collection.indexed) ||
26 (filters.indexFilter === 'not-indexed' && !collection.indexed);
27 
28 return matchesSearch && matchesIndex;
29 });
30}

Why this is a Strong-Hire move:

  • Pure function, no React. filterCollections takes inputs and returns output, no hooks, no side effects. That's why it's testable, and saying so out loud is the point.
  • It composes. Search and index-filter combine with a simple matchesSearch && matchesIndex. One function, one place to reason about "what's visible."
  • You wrote a test unprompted-ish. The round rewards testing. Following the template's own modules/ + *.spec.ts pattern shows you read the codebase and respect its conventions. Run npm run test and show it green.
  • Union type over booleans. 'all' | 'indexed' | 'not-indexed' makes illegal states unrepresentable, better than two loosely-related booleans.

Task 8: Load the collections from a real source

Prompt: Right now the list starts empty. Load the initial collections from an async source instead, and handle loading and errors.

This is the most common "where does it go next" escalation, because real MongoDB tools fetch from a database. Keep it honest about the three states every async read has: loading, error, and loaded.

Plan: "I'll add a loadCollections() module that returns a Promise, a stand-in for a driver/API call. App calls it once in a useEffect on mount, tracking isLoading and loadError. I render a loading line, an error-with-retry, or the app. I'll also seed my nextId past the largest loaded id so new collections don't collide."

Task 8: useEffect on mount, with loading + error + retry

Explorer

loadCollections.tsnewsrc/modules/loadCollections.ts
1import { Collection } from '../types/Collection';
2 
3// Stand-in for a real data source. In production this is where you would
4// call the MongoDB Node driver (db.listCollections()) or your own API.
5const SEED: Collection[] = [
6 { id: 1, name: 'users', indexed: true },
7 { id: 2, name: 'orders', indexed: false },
8 { id: 3, name: 'products', indexed: true },
9];
10 
11export function loadCollections(): Promise<Collection[]> {
12 return new Promise((resolve, reject) => {
13 setTimeout(() => {
14 // Flip this to true to exercise the error + retry path in the UI.
15 const shouldFail = false;
16 if (shouldFail) {
17 reject(new Error('the database did not respond'));
18 return;
19 }
20 resolve(SEED);
21 }, 600);
22 });
23}

The details that read as senior:

  • All three states, explicitly. Loading, error (with a Retry that just calls load() again), and success. New grads usually forget the error path, handling it is a differentiator.
  • useEffect(..., []) for "on mount." Mention you know the empty dependency array means "run once after first render," and that React 18 StrictMode double-invokes effects in dev (so the fetch may fire twice locally, harmless here, but you'd guard real mutations).
  • Seed the id counter. nextId.current = max(loaded ids) + 1 avoids id collisions between loaded and newly-created collections, a subtle correctness point most people miss.
  • Honest about the stand-in. Say "in production this loadCollections is where I'd call the MongoDB Node driver's listCollections() or our API. I'm faking the latency so the UI states are real."

What actually earns a "Strong Hire"

After all the code, what they're really grading is mostly how you worked, not how much you finished. The signals MongoDB engineers consistently reward:

Your 90-minute game plan

  1. Minutes 0–5: Give the one-sentence mental model, point at main.ts / renderer.tsx / App.tsx, and confirm the app + tests run.
  2. Each task: Restate it → one-line plan → code in small steps → Cmd+R and verify → say what you'd do next.
  3. When stuck: think out loud and invite your pair in. It's a peer-programming round, collaboration is the thing being measured.
  4. If offered a test: take it. A green npm run test on a pure module is a quiet, strong finish.

You're not being asked to be a React savant. You're being asked to take a tiny, well-structured app and grow it calmly, correctly, and out loud, in MongoDB's own vocabulary. Build the Collection Manager a few times until the moves feel automatic, and you'll walk in ready. 🚀

Was this page helpful?