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
- Board: Show the lists side by side, starting with five seeded lists (To do, Doing, In Review, Done, Backlog).
- Add list: An "Add another list" button opens a form. Empty titles are ignored.
- Add card: Each list has an "Add a card" form that appends a card to the end of that list. Empty text is ignored.
- 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.
- 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 (
listsand theircards) in a Redux store and update it with pure reducers. - Use
react-dndwith 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 storeuseSelector: 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
-
App Component
- Sets up Redux Provider and DnD Provider
- Renders the header and TrelloBoard
-
TrelloBoard Component
- Retrieves lists from Redux store
- Renders TrelloList components
- Renders AddList component
-
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
-
TrelloCard Component
- Receives card data as props
- Acts as both drag source and drop target
- Renders card content
-
ListActions Component
- Manages dropdown menu for list operations
- Triggers list actions (delete, etc.)
-
AddList Component
- Manages form state for creating new lists
- Dispatches addList action when submitted
Event Flow for Common Operations
Adding a New Card
- User clicks "+ Add a card" button in a list
toggleFormfunction is called, settingshowFormto true- Form renders, user enters card content
- User clicks "Add Card" button
handleAddCardfunction is called- Function checks if content is not empty
- If valid, dispatches
addCardaction with list ID and content - 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
- React re-renders with updated state
- Form is hidden, ready for next card
Dragging a Card Between Lists
- User starts dragging a card
useDraghook activates, settingisDraggingto true- Card's opacity reduces to provide visual feedback
- Cards are only drag sources. Dropping a card anywhere on a list runs that list's
drophandler. - The handler skips a drop that would leave the card where it is, then dispatches
moveCardto put the card at the end of the target list. - Redux reducer handles the move:
- For same-list moves: reorders the cards array
- For between-list moves: removes from source, adds to destination
- React re-renders with updated state
- 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:
-
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: [] } ] -
User Action: User starts dragging "Kickoff meeting" card.
-
Component Response:
- TrelloCard's
useDraghook activates - Card opacity reduces to 0.5
- Drag item created with:
{ id: "card-2", index: 1, listId: "list-1", content: "Kickoff meeting" }
- TrelloCard's
-
Dragging Over Target List:
- User drags over "Doing" list
- TrelloList's
useDrophover handler activates - Checks if card is from a different list (it is)
- Determines drop index (0, as list is empty)
-
Drop Action:
- User releases card over "Doing" list
- Drop handler dispatches action:
moveCard("card-2", "list-1", "list-2", 1, 0)
-
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
-
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: [] } ] -
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
-
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.
-
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(). -
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.
-
Dispatch: Dispatching an action is the process of sending an action to the store.
-
Provider: The
Providercomponent makes the Redux store available to any nested components that have been wrapped in theconnect()function.
Redux Data Flow
Redux follows a unidirectional data flow:
- Action is dispatched.
- Reducer processes the action and updates the state.
- The updated state is passed to the React components.
- The UI re-renders based on the new state.
1. Initial Setup
Store Creation
- The Redux store is created using the
reducerand is made available to the entire React application via theProvidercomponent.
import { Provider } from "react-redux";
import store from "./redux/store";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);- The
storeholds the initial state (initialData) and thereducerfunction.
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 apayload(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 likeuseDispatch.
dispatch(addList("New List Title"));- This sends the action to the reducer.
3. Reducer Processes the Action
Reducer Function
- The
reducerfunction 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_LISTaction 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
listsarray.
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
useSelectoror theconnectfunction.
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,
useSelectorre-runs and returns the updated state. - The component re-renders to reflect the new state.
6. Example Flow: Adding a List
-
User Interaction:
- The user clicks a button to add a new list.
-
Action Creator:
- The
addListaction creator is called with the list title.
dispatch(addList("Groceries")); - The
-
Action Dispatched:
- The action is sent to the Redux store.
{ type: ADD_LIST, payload: { title: "Groceries" }, } -
Reducer Processes the Action:
- The reducer handles the
ADD_LISTaction:- Creates a new list object with a unique ID and the title "Groceries".
- Returns a new state with the new list added to the
listsarray.
{ lists: [ ...state.lists, { id: "list-123", title: "Groceries", cards: [] }, ], } - The reducer handles the
-
State Updated:
- The Redux store updates its state with the new list.
-
UI Re-renders:
- React components connected to the store (e.g., using
useSelector) re-render to display the new list.
- React components connected to the store (e.g., using
7. Example Flow: Moving a Card
-
User Interaction:
- The user drags a card from one list to another.
-
Action Creator:
- The
moveCardaction creator is called with details about the card movement.
dispatch( moveCard( "card-123", // cardId "list-1", // sourceListId "list-2", // destinationListId 0, // sourceIndex 1 // destinationIndex ) ); - The
-
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, }, } -
Reducer Processes the Action:
- The reducer handles the
MOVE_CARDaction:- 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.
- The reducer handles the
-
State Updated:
- The Redux store updates its state with the new lists.
-
UI Re-renders:
- React components re-render to reflect the updated card positions.
Summary of the Flow
- Action Creator: Called in response to user interaction.
- Action Dispatched: Sent to the Redux store.
- Reducer Processes Action: Updates the state based on the action.
- State Updated: The store holds the new state.
- UI Re-renders: Components connected to the store update to reflect the new state.