Transfer List (Advanced + Reorder)
Difficulty: Advanced
Problem Statementβ
Extend transfer list with multi-select, move between lists, and reorder selected items up/down.
Live Demoβ
Try the running app below β this is the target behavior for the challenge.
Transfer List (Advanced + Reorder) β Live Demo
What Interviewers Expectβ
- Multi-select with Set.
- Reorder only valid selections.
- Disable move/reorder at boundaries.
Step-by-Step Implementationβ
Step 1 β Reorder algorithmβ
Swap selected items one position up/down without mutating array in place incorrectly.
[arr[i - 1], arr[i]] = [arr[i], arr[i - 1]];
Step 2 β Boundary checksβ
Disable up/down when selected item is at edge.
if (i > 0 && !sel.has(arr[i - 1])) swap...
Step 3 β Move selected batchβ
Move all selected items between lists in one action.
setFrom(from.filter((x) => !selected.has(x)));
Complete Implementationβ
Copy-paste ready single-file solution. Use this as your reference after attempting the challenge yourself, or paste it into CodeSandbox / StackBlitz to run locally.
App.jsx
import React, {useState} from 'react';
export default function App() {
const [list1, setList1] = useState(['A', 'B', 'C']);
const [list2, setList2] = useState([]);
const [sel1, setSel1] = useState(new Set());
const [sel2, setSel2] = useState(new Set());
const moveSelected = (from, setFrom, to, setTo, selected, clearSelected) => {
const moving = from.filter((item) => selected.has(item));
if (!moving.length) return;
setFrom(from.filter((item) => !selected.has(item)));
setTo([...to, ...moving]);
clearSelected(new Set());
};
const reorder = (items, setItems, selected, direction) => {
setItems((prev) => {
const arr = [...prev];
const indexes = arr
.map((item, index) => (selected.has(item) ? index : -1))
.filter((index) => index >= 0);
if (!indexes.length) return arr;
if (direction === 'up') {
for (const index of indexes) {
if (index > 0 && !selected.has(arr[index - 1])) {
[arr[index - 1], arr[index]] = [arr[index], arr[index - 1]];
}
}
} else {
for (let i = indexes.length - 1; i >= 0; i -= 1) {
const index = indexes[i];
if (index < arr.length - 1 && !selected.has(arr[index + 1])) {
[arr[index + 1], arr[index]] = [arr[index], arr[index + 1]];
}
}
}
return arr;
});
};
const renderList = (list, selected, setSelected, setList, otherList, setOther, clearOther) => (
<div
style={{
flex: 1,
minWidth: 220,
border: '1px solid #ccc',
borderRadius: 8,
padding: '0.75rem',
}}
>
<div style={{display: 'flex', gap: '0.5rem', marginBottom: '0.5rem'}}>
<button type="button" onClick={() => reorder(list, setList, selected, 'up')}>
β
</button>
<button type="button" onClick={() => reorder(list, setList, selected, 'down')}>
β
</button>
</div>
<ul style={{listStyle: 'none', padding: 0}}>
{list.map((item) => (
<li key={item} style={{display: 'flex', gap: '0.5rem', marginBottom: '0.35rem'}}>
<input
type="checkbox"
checked={selected.has(item)}
onChange={() => {
setSelected((prev) => {
const next = new Set(prev);
if (next.has(item)) next.delete(item);
else next.add(item);
return next;
});
}}
/>
<button
type="button"
onClick={() => {
setList(list.filter((value) => value !== item));
setOther([...otherList, item]);
clearOther(new Set());
}}
>
{item}
</button>
</li>
))}
</ul>
</div>
);
return (
<div style={{display: 'flex', gap: '0.75rem', alignItems: 'flex-start'}}>
{renderList(list1, sel1, setSel1, setList1, list2, setList2, setSel1)}
<div style={{display: 'grid', gap: '0.5rem', alignSelf: 'center'}}>
<button
type="button"
disabled={!sel1.size}
onClick={() => moveSelected(list1, setList1, list2, setList2, sel1, setSel1)}
>
β
</button>
<button
type="button"
disabled={!sel2.size}
onClick={() => moveSelected(list2, setList2, list1, setList1, sel2, setSel2)}
>
β
</button>
</div>
{renderList(list2, sel2, setSel2, setList2, list1, setList1, setSel2)}
</div>
);
}
Final Checklistβ
- UI works for primary flow
- Edge cases handled (empty/disabled/loading where relevant)
- State updates are immutable
- Components are readable and reasonably split
- You can explain trade-offs out loud in an interview
Next Challengeβ
Continue to the next item in the sidebar when you're comfortable implementing this from scratch in 30β45 minutes.