Skip to content

Transfer List

Move checked items between two lists with buttons for selected items and for all items.

Stage 4core3 min read

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

  1. Two lists: The left list starts with React, Node and MongoDB. The right list starts with HTML, CSS, JS and Typescript.
  2. Select items: Each item has a checkbox that the user can toggle.
  3. 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.
  4. 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 } in useState, and update it without mutation.
  • Give each button an aria-label, since the visible text is only arrows.

Table of Contents

  1. React Hooks Used
  2. JavaScript Methods Explained
  3. Component Breakdown
  4. Complete Code
  5. Dry Run Example

React Hooks Used

useState

const [itemsLeft, setItemsLeft] = useState(
  generateItemsObject(DEFAULT_ITEMS_LEFT)
);
  • Purpose: Manages component state
  • Usage:
    • itemsLeft: Current state of left list items
    • setItemsLeft: 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

MethodPurposeExample
Object.entries()Converts object to array of [key, value] pairsObject.entries({a:1}) → [['a', 1]]
Object.fromEntries()Converts array of entries back to objectObject.fromEntries([['a',1]]) → {a:1}
Object.values()Returns array of object's valuesObject.values({a:1,b:2}) → [1,2]
Object.keys()Returns array of object's keysObject.keys({a:1}) → ['a']

Array Methods

MethodPurposeExample
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 (itemsLeft and itemsRight)
  • 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 CheckedBoxItem components

3. CheckedBoxItem Component

  • Renders individual checkbox with label
  • Uses useId for 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">&lt;&lt;</span>
      </button>
      <button
        disabled={hasNoSelectedItems(itemsRight)}
        onClick={() =>
          transferSelectedItems(
            itemsRight,
            setItemsRight,
            itemsLeft,
            setItemsLeft
          )
        }
      >
        <span aria-hidden="true">&lt;</span>
      </button>
      <button
        disabled={hasNoSelectedItems(itemsLeft)}
        onClick={() =>
          transferSelectedItems(
            itemsLeft,
            setItemsLeft,
            itemsRight,
            setItemsRight
          )
        }
      >
        <span aria-hidden="true">&gt;</span>
      </button>
      <button
        onClick={() =>
          transferAllItems(itemsLeft, setItemsLeft, itemsRight, setItemsRight)
        }
        disabled={Object.keys(itemsLeft).length === 0}
      >
        <span aria-hidden="true">&gt;&gt;</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)

  1. 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
    }
  2. 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: true

Dry 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: false

3. 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

FunctionInputActionOutput
generateItemsObject["a","b"]Create object{a:false, b:false}
hasNoSelectedItems{a:false, b:true}Check selectionsfalse
transferAllItemsSource: {a:true}, Target: {b:false}Move allSource: {}, Target: {a:true, b:false}
transferSelectedItemsSource: {a:true, b:false}, Target: {c:true}Move selectedSource: {b:false}, Target: {a:true, c:true}
Checkbox Toggle{a:false} → toggle 'a'Flip boolean{a:true}

In this topic

Done with this topic?

How well do you know it?

PreviousMovie Seat Selection