Skip to content

Trello Board

A Trello style board where you add lists and cards, archive a list, and drag a card onto another list, with board data kept in a Redux store.

Builds on Drag and Drop Board

Stage 4advanced9 min read
The board with To do, Doing and Done listsZoom

Problem Statement

Build a Trello style board with lists of cards. Users can add lists and cards, archive a list, and drag a card from one list to another.

Functional Requirements

  1. Board: Show the lists side by side, starting with five seeded lists (To do, Doing, In Review, Done, Backlog).
  2. Add list: An "Add another list" button opens a form. Empty titles are ignored.
  3. Add card: Each list has an "Add a card" form that appends a card to the end of that list. Empty text is ignored.
  4. Drag and drop: Dragging a card onto a list moves it to the end of that list. The dragged card fades to 0.5 opacity.
  5. List actions: The ... button opens a menu. "Archive this list" removes the list and its cards. The other items only close the menu.

Technical Constraints

  • Keep the board data (lists and their cards) in a Redux store and update it with pure reducers.
  • Use react-dnd with the HTML5 backend for drag and drop.

Functional Components

All components in our application are functional components that follow React's modern approach:

const TrelloCard = ({ card, index, listId }) => {
  // Component logic here
  return (
    // JSX here
  );
};

React Hooks

useState

Used to manage local component state:

const [showForm, setShowForm] = useState(false);
const [cardContent, setCardContent] = useState("");

Common use cases in our app:

  • Toggle form visibility
  • Store form input values
  • Track UI states like open/closed menus

useRef

Used to access and interact with DOM elements directly:

const ref = useRef(null);

In our app, used primarily for:

  • Getting DOM measurements during drag operations
  • Creating DOM references for drag and drop functionality

useDispatch/useSelector (Redux Hooks)

Used to interact with the Redux store:

const dispatch = useDispatch();
const lists = useSelector((state) => state.lists);
  • useDispatch: Dispatches actions to update the store
  • useSelector: Extracts data from the store

Custom Hooks from React DnD

const [{ isDragging }, drag] = useDrag({
  // Configuration
});

const [{ isOver }, drop] = useDrop({
  // Configuration
});

Conditional Rendering

Used extensively to show/hide elements based on state:

{
  showForm ? (
    <div className="add-card-form">{/* Form elements */}</div>
  ) : (
    <button className="add-card-button" onClick={toggleForm}>
      + Add a card
    </button>
  );
}

Component Composition

Our application follows a hierarchical structure:

  • App contains TrelloBoard
  • TrelloBoard contains multiple TrelloList components
  • TrelloList contains multiple TrelloCard components

Core JavaScript Concepts Used

ES6+ Features

Arrow Functions

Used for concise function expressions:

const toggleForm = () => setShowForm(!showForm);

Destructuring Assignment

Extract values from objects and arrays:

const { id, listId, index } = item; // the dragged card, from useDrag
const [{ isDragging }, drag] = useDrag({...});

Spread Operator

Used for immutable state updates:

return {
  ...state,
  lists: [...state.lists, newList],
};

Template Literals

Used for dynamic class names:

className={`trello-card ${isDragging ? "is-dragging" : ""}`}

Array Methods

  • map: Transform lists into components

    lists.map((list) => <TrelloList key={list.id} list={list} />);
  • filter: Remove items

    state.lists.filter((list) => list.id !== action.payload.listId);
  • find: Locate specific items

    const list = state.lists.find((list) => list.id === sourceListId);
  • splice: Modify arrays for card reordering

    const [movedCard] = sourceCards.splice(sourceIndex, 1);
    destCards.splice(destinationIndex, 0, movedCard);

Closures

Used in event handlers and callbacks where functions access variables from their parent scope:

const handleAddCard = () => {
  if (cardContent.trim()) {
    dispatch(addCard(list.id, cardContent));
    setCardContent("");
    setShowForm(false);
  }
};

DOM Operations

Not in this code yet. To reorder cards inside a list, a card would also be a drop target and compare the pointer with its middle:

const hoverBoundingRect = ref.current.getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = clientOffset.y - hoverBoundingRect.top;

Redux Implementation

Store Structure

{
  lists: [
    {
      id: "list-1",
      title: "To do",
      cards: [
        { id: "card-1", content: "Project planning" },
        { id: "card-2", content: "Kickoff meeting" },
      ],
    },
    // More lists...
  ];
}

Action Types

Constants defining the available actions:

export const ADD_LIST = "ADD_LIST";
export const ADD_CARD = "ADD_CARD";
export const MOVE_CARD = "MOVE_CARD";
export const DELETE_LIST = "DELETE_LIST";
export const EDIT_LIST_TITLE = "EDIT_LIST_TITLE";

Action Creators

Functions that return action objects:

export const addCard = (listId, content) => ({
  type: ADD_CARD,
  payload: { listId, content },
});

export const moveCard = (
  cardId,
  sourceListId,
  destinationListId,
  sourceIndex,
  destinationIndex
) => ({
  type: MOVE_CARD,
  payload: {
    cardId,
    sourceListId,
    destinationListId,
    sourceIndex,
    destinationIndex,
  },
});

Reducer

A pure function handling state updates:

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_LIST: {
      // Create new list and add to state
    }
    case ADD_CARD: {
      // Add card to specified list
    }
    case MOVE_CARD: {
      // Move card within or between lists
    }
    // Other cases...
    default:
      return state;
  }
};

Key patterns in the reducer:

  • Always return a new state object
  • Use immutable update patterns
  • Handle complex state transitions for moves

React DnD Implementation

DnD Provider Setup

<DndProvider backend={HTML5Backend}>
  <div className="app">{/* Application components */}</div>
</DndProvider>

Card as Drag Source

Each card is only a drag source. Lists are the drop targets (next section), so a dropped card always goes to the end of a list.

const TrelloCard = ({ card, index, listId }) => {
  const [{ isDragging }, drag] = useDrag({
    type: ItemTypes.CARD,
    item: { id: card.id, index, listId, content: card.content },
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
  });

  return (
    <div
      ref={drag}
      className={`trello-card ${isDragging ? "is-dragging" : ""}`}
      style={{ opacity: isDragging ? 0.5 : 1 }}
    >
      <div className="card-content">{card.content}</div>
    </div>
  );
};

To reorder cards inside a list, the card would also call useDrop with a hover handler that compares the pointer with the card's middle (see DOM Operations above) and dispatches moveCard once it crosses.

List as Drop Target

const TrelloList = ({ list }) => {
  // Component state and dispatch

  // Make the list a drop target for cards
  const [{ isOver }, drop] = useDrop({
    accept: "CARD",
    drop: (item, monitor) => {
      // Handle card drops on the list (not on other cards)
    },
    collect: (monitor) => ({
      isOver: monitor.isOver({ shallow: true }),
    }),
  });

  return (
    <div className="trello-list">
      <div className="list-header">{/* List header content */}</div>

      <div
        ref={drop}
        className={`cards-container ${isOver ? "dragging-over" : ""}`}
      >
        {/* List cards */}
      </div>

      {/* Add card form or button */}
    </div>
  );
};

Application Flow

Component Hierarchy and Data Flow

  1. App Component

    • Sets up Redux Provider and DnD Provider
    • Renders the header and TrelloBoard
  2. TrelloBoard Component

    • Retrieves lists from Redux store
    • Renders TrelloList components
    • Renders AddList component
  3. TrelloList Component

    • Receives list data as props
    • Manages list-specific state (form visibility, etc.)
    • Acts as a drop target for cards
    • Renders TrelloCard components
    • Renders add card form/button
  4. TrelloCard Component

    • Receives card data as props
    • Acts as both drag source and drop target
    • Renders card content
  5. ListActions Component

    • Manages dropdown menu for list operations
    • Triggers list actions (delete, etc.)
  6. AddList Component

    • Manages form state for creating new lists
    • Dispatches addList action when submitted

Event Flow for Common Operations

Adding a New Card

  1. User clicks "+ Add a card" button in a list
  2. toggleForm function is called, setting showForm to true
  3. Form renders, user enters card content
  4. User clicks "Add Card" button
  5. handleAddCard function is called
  6. Function checks if content is not empty
  7. If valid, dispatches addCard action with list ID and content
  8. Redux reducer handles the action:
    • Creates a new card object with UUID
    • Finds the target list
    • Creates a new state with the card added to the list
  9. React re-renders with updated state
  10. Form is hidden, ready for next card

Dragging a Card Between Lists

  1. User starts dragging a card
  2. useDrag hook activates, setting isDragging to true
  3. Card's opacity reduces to provide visual feedback
  4. Cards are only drag sources. Dropping a card anywhere on a list runs that list's drop handler.
  5. The handler skips a drop that would leave the card where it is, then dispatches moveCard to put the card at the end of the target list.
  6. Redux reducer handles the move:
    • For same-list moves: reorders the cards array
    • For between-list moves: removes from source, adds to destination
  7. React re-renders with updated state
  8. Drag ends, card appears in new position

Dry Run Example

Let's walk through the process of dragging a card from "To Do" list to "Doing" list:

  1. Initial State:

    Lists: [
      { id: "list-1", title: "To do", cards: [
        { id: "card-1", content: "Project planning" },
        { id: "card-2", content: "Kickoff meeting" }
      ] },
      { id: "list-2", title: "Doing", cards: [] },
      { id: "list-3", title: "Done", cards: [] }
    ]
    
  2. User Action: User starts dragging "Kickoff meeting" card.

  3. Component Response:

    • TrelloCard's useDrag hook activates
    • Card opacity reduces to 0.5
    • Drag item created with:
      { id: "card-2", index: 1, listId: "list-1", content: "Kickoff meeting" }
      
  4. Dragging Over Target List:

    • User drags over "Doing" list
    • TrelloList's useDrop hover handler activates
    • Checks if card is from a different list (it is)
    • Determines drop index (0, as list is empty)
  5. Drop Action:

    • User releases card over "Doing" list
    • Drop handler dispatches action:
      moveCard("card-2", "list-1", "list-2", 1, 0)
      
  6. Reducer Processing:

    • Identifies this as a between-list move
    • Finds source list ("To do")
    • Finds destination list ("Doing")
    • Creates copies of both card arrays
    • Removes card from source array
    • Adds card to destination array at index 0
    • Creates new state with updated lists
  7. Updated State:

    Lists: [
      { id: "list-1", title: "To do", cards: [
        { id: "card-1", content: "Project planning" }
      ] },
      { id: "list-2", title: "Doing", cards: [
        { id: "card-2", content: "Kickoff meeting" }
      ] },
      { id: "list-3", title: "Done", cards: [] }
    ]
    
  8. React Re-render:

    • TrelloBoard re-renders with new state
    • TrelloList components update to show new card arrangements
    • Animation completes, card now appears in "Doing" list

Redux data flow, step by step

Key Concepts in Redux

  1. Store: The store holds the whole state tree of your application. The only way to change the state inside it is to dispatch an action on it.

  2. Actions: Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().

  3. Reducers: Reducers specify how the application's state changes in response to actions sent to the store. Remember that actions only describe what happened, but don't describe how the application's state changes.

  4. Dispatch: Dispatching an action is the process of sending an action to the store.

  5. Provider: The Provider component makes the Redux store available to any nested components that have been wrapped in the connect() function.

Redux Data Flow

Redux follows a unidirectional data flow:

  1. Action is dispatched.
  2. Reducer processes the action and updates the state.
  3. The updated state is passed to the React components.
  4. The UI re-renders based on the new state.

1. Initial Setup

Store Creation

  • The Redux store is created using the reducer and is made available to the entire React application via the Provider component.
import { Provider } from "react-redux";
import store from "./redux/store";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);
  • The store holds the initial state (initialData) and the reducer function.

2. Dispatching an Action

Action Creator

  • When a user interacts with the UI (e.g., clicks a button to add a list), an action creator is called. For example:
export const addList = (title) => ({
  type: ADD_LIST,
  payload: { title },
});
  • This action creator returns an action object with a type (describing what happened) and a payload (data needed to update the state).

Dispatching the Action

  • The action is dispatched to the Redux store using store.dispatch() or, in React components, using hooks like useDispatch.
dispatch(addList("New List Title"));
  • This sends the action to the reducer.

3. Reducer Processes the Action

Reducer Function

  • The reducer function takes the current state and the action as arguments and returns the new state based on the action type.
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_LIST: {
      const newList = {
        id: `list-${uuidv4()}`,
        title: action.payload.title,
        cards: [],
      };
      return {
        ...state,
        lists: [...state.lists, newList],
      };
    }
    // Other cases...
    default:
      return state;
  }
};
  • For example, when the ADD_LIST action is dispatched:
    • The reducer creates a new list object with a unique ID and the title from the action payload.
    • It returns a new state object with the new list added to the lists array.

4. State Update

  • The Redux store updates its state with the new state returned by the reducer.
  • The store notifies all connected components that the state has changed.

5. React Components Re-render

Connecting Components to the Store

  • React components can access the Redux store using hooks like useSelector or the connect function.
import { useSelector } from "react-redux";

const MyComponent = () => {
  const lists = useSelector((state) => state.lists);
  return (
    <div>
      {lists.map((list) => (
        <div key={list.id}>{list.title}</div>
      ))}
    </div>
  );
};
  • When the state changes, useSelector re-runs and returns the updated state.
  • The component re-renders to reflect the new state.

6. Example Flow: Adding a List

  1. User Interaction:

    • The user clicks a button to add a new list.
  2. Action Creator:

    • The addList action creator is called with the list title.
    dispatch(addList("Groceries"));
  3. Action Dispatched:

    • The action is sent to the Redux store.
    {
      type: ADD_LIST,
      payload: { title: "Groceries" },
    }
  4. Reducer Processes the Action:

    • The reducer handles the ADD_LIST action:
      • Creates a new list object with a unique ID and the title "Groceries".
      • Returns a new state with the new list added to the lists array.
    {
      lists: [
        ...state.lists,
        { id: "list-123", title: "Groceries", cards: [] },
      ],
    }
  5. State Updated:

    • The Redux store updates its state with the new list.
  6. UI Re-renders:

    • React components connected to the store (e.g., using useSelector) re-render to display the new list.

7. Example Flow: Moving a Card

  1. User Interaction:

    • The user drags a card from one list to another.
  2. Action Creator:

    • The moveCard action creator is called with details about the card movement.
    dispatch(
      moveCard(
        "card-123", // cardId
        "list-1", // sourceListId
        "list-2", // destinationListId
        0, // sourceIndex
        1 // destinationIndex
      )
    );
  3. Action Dispatched:

    • The action is sent to the Redux store.
    {
      type: MOVE_CARD,
      payload: {
        cardId: "card-123",
        sourceListId: "list-1",
        destinationListId: "list-2",
        sourceIndex: 0,
        destinationIndex: 1,
      },
    }
  4. Reducer Processes the Action:

    • The reducer handles the MOVE_CARD action:
      • Finds the source and destination lists.
      • Moves the card from the source list to the destination list.
      • Returns a new state with the updated lists.
  5. State Updated:

    • The Redux store updates its state with the new lists.
  6. UI Re-renders:

    • React components re-render to reflect the updated card positions.

Summary of the Flow

  1. Action Creator: Called in response to user interaction.
  2. Action Dispatched: Sent to the Redux store.
  3. Reducer Processes Action: Updates the state based on the action.
  4. State Updated: The store holds the new state.
  5. UI Re-renders: Components connected to the store update to reflect the new state.

In this topic

Done with this topic?

How well do you know it?

PreviousFile Explorer