Problem Statement
Build a transfer list. Show two lists of checkbox items with four buttons between them to move items from one list to the other.
Functional Requirements
- Two lists: The left list starts with React, Node and MongoDB. The right list starts with HTML, CSS, JS and Typescript.
- Select items: Each item has a checkbox that the user can toggle.
- Move selected:
<and>move only the checked items to the other list, and they stay checked there. Each button is disabled when its source list has nothing checked. - Move all:
<<and>>move every item to the other list. Each button is disabled when its source list is empty.
Technical Constraints
- Store each list as an object of
{ [label]: checked }inuseState, and update it without mutation. - Give each button an
aria-label, since the visible text is only arrows.
Table of Contents
React Hooks Used
useState
const [itemsLeft, setItemsLeft] = useState(
generateItemsObject(DEFAULT_ITEMS_LEFT)
);- Purpose: Manages component state
- Usage:
itemsLeft: Current state of left list itemssetItemsLeft: Function to update left list state- Initial state is created by
generateItemsObject
useId
const id = useId();- Purpose: Generates unique IDs for accessibility
- Usage: Creates unique IDs for checkbox-label pairing in
CheckedBoxItem
JavaScript Methods Explained
Object Methods
| Method | Purpose | Example |
|---|---|---|
Object.entries() | Converts object to array of [key, value] pairs | Object.entries({a:1}) → [['a', 1]] |
Object.fromEntries() | Converts array of entries back to object | Object.fromEntries([['a',1]]) → {a:1} |
Object.values() | Returns array of object's values | Object.values({a:1,b:2}) → [1,2] |
Object.keys() | Returns array of object's keys | Object.keys({a:1}) → ['a'] |
Array Methods
| Method | Purpose | Example |
|---|---|---|
reduce() | Transforms array into single value | [1,2].reduce((a,b)=>a+b,0) → 3 |
filter() | Creates new array with matching items | [1,2].filter(x=>x>1) → [2] |
every() | Tests if all elements pass test | [1,2].every(x=>x>0) → true |
map() | Transforms array elements | [1,2].map(x=>x*2) → [2,4] |
Component Breakdown
1. App Component
- Manages two lists (
itemsLeftanditemsRight) - Contains transfer logic functions
- Renders transfer buttons and both item lists
2. ItemList Component
- Displays a list of checkboxes
- Receives items and setter function as props
- Maps through items to render
CheckedBoxItemcomponents
3. CheckedBoxItem Component
- Renders individual checkbox with label
- Uses
useIdfor accessibility - Handles checkbox toggle events
Complete Code
// App.js
import { useState } from "react";
import ItemList from "./components/ItemList";
const DEFAULT_ITEMS_RIGHT = ["HTML", "CSS", "JS", "Typescript"];
const DEFAULT_ITEMS_LEFT = ["React", "Node", "MongoDB"];
const App = () => {
// Converts array to object with false values
function generateItemsObject(items) {
return items.reduce((acc, item) => {
acc[item] = false;
return acc;
}, {});
}
// Checks if no items are selected
function hasNoSelectedItems(items) {
return Object.values(items).every((item) => !item);
}
// Transfers all items between lists
function transferAllItems(
itemSource,
setItemSource,
itemTarget,
setItemTarget
) {
setItemTarget((prevItems) => ({ ...prevItems, ...itemSource }));
setItemSource({});
}
// Transfers only selected items between lists
function transferSelectedItems(
itemSource,
setItemSource,
itemTarget,
setItemTarget
) {
setItemTarget((prevItems) => ({
...prevItems,
...Object.fromEntries(
Object.entries(itemSource).filter(([_, checked]) => checked)
),
}));
setItemSource((prevItems) =>
Object.fromEntries(
Object.entries(prevItems).filter(([_, checked]) => !checked)
)
);
}
const [itemsLeft, setItemsLeft] = useState(
generateItemsObject(DEFAULT_ITEMS_LEFT)
);
const [itemsRight, setItemsRight] = useState(
generateItemsObject(DEFAULT_ITEMS_RIGHT)
);
return (
<div className="transfer-list">
<ItemList items={itemsLeft} setItems={setItemsLeft} />
<button
onClick={() =>
transferAllItems(itemsRight, setItemsRight, itemsLeft, setItemsLeft)
}
disabled={Object.keys(itemsRight).length === 0}
>
<span aria-hidden="true"><<</span>
</button>
<button
disabled={hasNoSelectedItems(itemsRight)}
onClick={() =>
transferSelectedItems(
itemsRight,
setItemsRight,
itemsLeft,
setItemsLeft
)
}
>
<span aria-hidden="true"><</span>
</button>
<button
disabled={hasNoSelectedItems(itemsLeft)}
onClick={() =>
transferSelectedItems(
itemsLeft,
setItemsLeft,
itemsRight,
setItemsRight
)
}
>
<span aria-hidden="true">></span>
</button>
<button
onClick={() =>
transferAllItems(itemsLeft, setItemsLeft, itemsRight, setItemsRight)
}
disabled={Object.keys(itemsLeft).length === 0}
>
<span aria-hidden="true">>></span>
</button>
<ItemList items={itemsRight} setItems={setItemsRight} />
</div>
);
};
export default App;// ItemList.js
import CheckedBoxItem from "./CheckedBoxItem";
const ItemList = ({ items, setItems }) => {
return (
<div className="transfer-list__section">
<ul className="transfer-list__section__items">
{Object.entries(items).map(([label, checked]) => (
<li key={label}>
<CheckedBoxItem
label={label}
checked={checked}
onChange={() => {
setItems((prevItems) => ({
...prevItems,
[label]: !prevItems[label],
}));
}}
/>
</li>
))}
</ul>
</div>
);
};
export default ItemList;// CheckedBoxItem.js
import React, { useId } from "react";
const CheckedBoxItem = ({ label, onChange, checked }) => {
const id = useId();
return (
<div>
<input type="checkbox" id={id} checked={checked} onChange={onChange} />
<label htmlFor={id}>{label}</label>
</div>
);
};
export default CheckedBoxItem;Dry Run Example
Initial State
itemsLeft = {
React: true, // Selected
Node: false,
MongoDB: false,
};
itemsRight = {
HTML: false,
CSS: true, // Selected
JS: false,
Typescript: false,
};Action: Click ">" (Transfer selected left to right)
-
Transfer to Right List:
// Get selected items from left Object.entries(itemsLeft) → [ ["React",true], ["Node",false], ["MongoDB",false] ] .filter(([_, checked]) => checked) → [ ["React",true] ] Object.fromEntries() → { "React": true } // Merge with right items itemsRight = { ...itemsRight, "React": true } -
Update Left List:
// Keep only unselected items Object.entries(itemsLeft) → [ ["React",true], ["Node",false], ["MongoDB",false] ] .filter(([_, checked]) => !checked) → [ ["Node",false], ["MongoDB",false] ] Object.fromEntries() → { "Node": false, "MongoDB": false }
Resulting State
itemsLeft = {
Node: false,
MongoDB: false,
};
itemsRight = {
HTML: false,
CSS: true,
JS: false,
Typescript: false,
React: true, // Transferred item
};UI Representation
Before Transfer:
Left List Right List
[✓] React [ ] HTML
[ ] Node [✓] CSS
[ ] MongoDB [ ] JS
[ ] Typescript
After Transfer:
Left List Right List
[ ] Node [ ] HTML
[ ] MongoDB [✓] CSS
[ ] JS
[ ] Typescript
[✓] React
Comprehensive Dry Run of All Transfer List Functions
Let's examine each function in detail with complete dry runs, including initial states and step-by-step execution.
1. generateItemsObject
Purpose: Converts an array of strings into an object with {item: false} format
Dry Run:
const DEFAULT_ITEMS_LEFT = ["React", "Node", "MongoDB"];
// Execution:
generateItemsObject(DEFAULT_ITEMS_LEFT)
// Step-by-step:
1. Start with empty accumulator {}
2. First item "React" → {"React": false}
3. Second item "Node" → {"React": false, "Node": false}
4. Third item "MongoDB" → {"React": false, "Node": false, "MongoDB": false}
// Final Output:
{
"React": false,
"Node": false,
"MongoDB": false
}2. hasNoSelectedItems
Purpose: Checks if no items are selected in a list
Dry Run 1: No selections
const items = {
"React": false,
"Node": false
};
// Execution:
hasNoSelectedItems(items)
// Steps:
1. Object.values(items) → [false, false]
2. .every(item => !item) → true && true → true
// Returns: trueDry Run 2: With selections
const items = {
"React": true,
"Node": false
};
// Execution:
hasNoSelectedItems(items)
// Steps:
1. Object.values(items) → [true, false]
2. .every(item => !item) → false && true → false
// Returns: false3. transferAllItems
Purpose: Moves all items from source to target list
Dry Run:
Initial State:
source = {
HTML: true,
CSS: false,
};
target = {
React: false,
};Execution:
transferAllItems(source, setSource, target, setTarget);Step 1: Update Target:
setTarget receives:
{
...target, // {"React": false}
...source // {"HTML": true, "CSS": false}
}
// New target:
{
"React": false,
"HTML": true,
"CSS": false
}Step 2: Clear Source:
setSource({});
// New source: {}Final State:
source = {};
target = {
React: false,
HTML: true,
CSS: false,
};4. transferSelectedItems
Purpose: Moves only selected items between lists
Dry Run:
Initial State:
source = {
React: true, // Selected
Node: false,
MongoDB: true, // Selected
};
target = {
HTML: false,
CSS: true, // Selected
};Execution:
transferSelectedItems(source, setSource, target, setTarget);Step 1: Update Target:
// Get selected items from source:
Object.entries(source) → [ ["React",true], ["Node",false], ["MongoDB",true] ]
.filter(([_, checked]) => checked) → [ ["React",true], ["MongoDB",true] ]
Object.fromEntries() → { "React": true, "MongoDB": true }
// Merge with target:
setTarget receives:
{
...target, // {"HTML": false, "CSS": true}
"React": true,
"MongoDB": true
}
// New target:
{
"HTML": false,
"CSS": true,
"React": true,
"MongoDB": true
}Step 2: Update Source:
// Keep only unselected items:
Object.entries(source) → [ ["React",true], ["Node",false], ["MongoDB",true] ]
.filter(([_, checked]) => !checked) → [ ["Node",false] ]
Object.fromEntries() → { "Node": false }
// New source:
{
"Node": false
}Final State:
source = {
Node: false,
};
target = {
HTML: false,
CSS: true,
React: true,
MongoDB: true,
};5. Checkbox Toggle (in ItemList)
Purpose: Toggles selection state of an individual item
Dry Run:
Initial State:
items = {
React: false,
Node: true,
};Action: Toggling "React" checkbox
Execution:
setItems((prevItems) => ({
...prevItems,
React: !prevItems["React"], // false → true
}));New State:
items = {
React: true, // Flipped from false
Node: true,
};Visual Summary of All Operations
| Function | Input | Action | Output |
|---|---|---|---|
generateItemsObject | ["a","b"] | Create object | {a:false, b:false} |
hasNoSelectedItems | {a:false, b:true} | Check selections | false |
transferAllItems | Source: {a:true}, Target: {b:false} | Move all | Source: {}, Target: {a:true, b:false} |
transferSelectedItems | Source: {a:true, b:false}, Target: {c:true} | Move selected | Source: {b:false}, Target: {a:true, c:true} |
| Checkbox Toggle | {a:false} → toggle 'a' | Flip boolean | {a:true} |