Kanban State Management: useState vs Redux
State management in a React kanban board determines how efficiently your app updates when tasks move, filters change, or new cards are added. The choice between useState, Context API, and Redux affects component re-rendering, code maintainability, and scalability. Small boards with few columns thrive on useState and Context; large boards with hundreds of cards and multiple users benefit from Redux's predictable state machine and debugging tools.
Comparing State Management Approaches
| Approach | Data Structure | Re-render Scope | Learning Curve | Best For |
|---|---|---|---|---|
useState | Nested objects/arrays | Entire component | Very low | Single board, <5 columns |
| Context API | Single context object | All subscribers | Low | Medium boards, no frequency updates |
| Redux | Centralized store + reducers | Only affected subscribers | Medium | Large boards, time-travel debug, team projects |
| Zustand / Jotai | Lightweight atoms | Granular (subscriber-based) | Low-medium | Modern alternative with less boilerplate |
Each approach trades simplicity for scalability. useState requires minimal setup but causes re-renders of entire component trees when state updates. Redux adds structure and enables powerful dev tools but introduces boilerplate. Context sits in between: better than useState for prop drilling, but slower than Redux for frequent updates (Context uses a global value, so all subscribers re-render when anything changes).
Using useState for Small Kanban Boards
For a personal productivity app or a prototype, useState is often the right choice. Keep your state structure flat and update immutably:
function Board() {
const [tasks, setTasks] = React.useState(initialTasks);
const [selectedTaskId, setSelectedTaskId] = React.useState(null);
const [filter, setFilter] = React.useState('all'); // all, todo, done
function moveTask(taskId, fromColumn, toColumn) {
setTasks((prev) => {
const newTasks = { ...prev };
const source = [...prev[fromColumn]];
const target = [...prev[toColumn]];
const index = source.findIndex((t) => t.id === taskId);
const [task] = source.splice(index, 1);
target.push(task);
return {
...newTasks,
[fromColumn]: source,
[toColumn]: target,
};
});
}
function addTask(columnId, title) {
setTasks((prev) => ({
...prev,
[columnId]: [
...prev[columnId],
{ id: Date.now().toString(), title, description: '' },
],
}));
}
const filteredTasks = {
todo: tasks.todo.filter((t) => filter === 'all' || filter === 'todo'),
inProgress: tasks.inProgress.filter(
(t) => filter === 'all' || filter === 'inProgress'
),
done: tasks.done.filter((t) => filter === 'all' || filter === 'done'),
};
return (
<div className="board">
<div className="controls">
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('todo')}>To Do</button>
<button onClick={() => setFilter('done')}>Done</button>
</div>
{/* Render columns with filteredTasks */}
</div>
);
}
This approach keeps all logic in one component. For a board with 3 columns and 20 tasks, the re-render cost is negligible. The trade-off: as your app grows (more filters, real-time updates, multiple boards), this single state object becomes unwieldy.
Scaling with Context API
When multiple distant components need the same state (e.g., a sidebar task list, a card detail modal, and the board all referencing the same tasks), Context API eliminates prop drilling:
const KanbanContext = React.createContext();
export function KanbanProvider({ children }) {
const [tasks, setTasks] = React.useState(initialTasks);
const [selectedTaskId, setSelectedTaskId] = React.useState(null);
const value = {
tasks,
selectedTaskId,
moveTask: (taskId, fromColumn, toColumn) => {
setTasks((prev) => {
// ... move logic
return updated;
});
},
updateTask: (taskId, updates) => {
setTasks((prev) => ({
...prev,
// map through and update the matching task
}));
},
};
return (
<KanbanContext.Provider value={value}>
{children}
</KanbanContext.Provider>
);
}
function useKanban() {
const context = React.useContext(KanbanContext);
if (!context) {
throw new Error('useKanban must be used within KanbanProvider');
}
return context;
}
// Usage in components
function Card({ taskId }) {
const { tasks, moveTask } = useKanban();
const task = tasks.todo.find((t) => t.id === taskId); // inefficient!
return <div>{task.title}</div>;
}
Context eliminates prop drilling but introduces a performance issue: every time tasks or any value in the provider changes, all components reading from that context re-render. For a kanban board with 100 cards and a frequent operation (drag reordering), this is wasteful. Only the card being moved needs to update; the context approach re-renders all cards.
Using Redux for Large, Complex Kanban Apps
Redux centralizes state in a store and updates it through reducers (pure functions that describe how state changes). This enables optimized re-renders, time-travel debugging, and middleware:
// actions.js
const MOVE_TASK = 'MOVE_TASK';
const ADD_TASK = 'ADD_TASK';
export function moveTask(taskId, fromColumn, toColumn) {
return { type: MOVE_TASK, payload: { taskId, fromColumn, toColumn } };
}
// reducer.js
const initialState = {
todo: [...],
inProgress: [...],
done: [...],
};
function kanbanReducer(state = initialState, action) {
switch (action.type) {
case MOVE_TASK: {
const { taskId, fromColumn, toColumn } = action.payload;
const source = state[fromColumn].filter((t) => t.id !== taskId);
const movedTask = state[fromColumn].find((t) => t.id === taskId);
const target = [...state[toColumn], movedTask];
return {
...state,
[fromColumn]: source,
[toColumn]: target,
};
}
case ADD_TASK: {
const { columnId, task } = action.payload;
return {
...state,
[columnId]: [...state[columnId], task],
};
}
default:
return state;
}
}
// store setup (using Redux Toolkit)
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: {
kanban: kanbanReducer,
},
});
// component usage
import { useDispatch, useSelector } from 'react-redux';
function Board() {
const dispatch = useDispatch();
const tasks = useSelector((state) => state.kanban);
function handleTaskDropped(taskId, fromColumn, toColumn) {
dispatch(moveTask(taskId, fromColumn, toColumn));
}
return <div className="board">{/* render columns */}</div>;
}
Redux Toolkit simplifies the boilerplate. Each action and reducer are explicit, making the state machine predictable. React-Redux's useSelector hook optimizes re-renders: only components that select changed data re-render. If you reorder a task in the todo column, only that column's cards update.
Performance: State Update Patterns
For kanban drag operations, how you update state affects performance. Measure with React DevTools Profiler:
// Inefficient: every update creates a new object, causing all consumers to re-render
const [tasks, setTasks] = React.useState(initialTasks);
// Efficient: use Redux selectors or useMemo to memoize derived state
const memoizedTasks = React.useMemo(
() => tasks,
[tasks]
);
// Better: split state by column if columns update independently
const [todoTasks, setTodoTasks] = React.useState(initialTasks.todo);
const [inProgressTasks, setInProgressTasks] = React.useState(
initialTasks.inProgress
);
// Best: Redux with granular selectors prevents unnecessary re-renders
const todoCards = useSelector(
(state) => state.kanban.todo,
shallowEqual // only re-render if the array itself changes
);
For a board with 500 tasks, splitting state or using Redux with granular selectors can improve drag responsiveness from 60 fps to 55+ fps (smoother perceived performance).
Decision Table: Which Pattern to Use?
| Scenario | Recommended Approach | Reason |
|---|---|---|
| Personal task list, <20 tasks | useState | Minimal overhead, easy to reason about |
| Team board, <100 tasks, static columns | Context API + useMemo | Eliminates prop drilling, acceptable performance |
| Enterprise kanban, 500+ tasks, real-time sync | Redux + middleware | Predictable state, middleware for async, dev tools |
| Very simple board, frequent updates | Zustand / Jotai | Lightweight, granular updates, less boilerplate than Redux |
Key Takeaways
- useState suffices for small boards: Simple, no dependencies, fast to build.
- Context API eliminates prop drilling: Trade-off is coarser re-render boundaries.
- Redux scales to enterprise: Structured, debuggable, middleware for complex flows.
- Measure performance: Profile re-renders; optimize if drag feels sluggish.
- Combine patterns: Use Redux for shared state, Context for UI theme, useState for local component state.
Frequently Asked Questions
Does Redux slow down my app compared to useState?
No. Redux with useSelector is typically faster because it updates only affected subscribers. useState with a single large object may re-render more components. Redux has a small setup cost, but pays off with hundreds of state changes.
Should I use Redux Toolkit or vanilla Redux?
Redux Toolkit (since 2019) is the official recommendation. It includes immer for immutable updates, configureStore for setup, and createSlice to merge actions and reducers, cutting boilerplate by 70%.
How do I manage async operations like fetching tasks from an API?
In Redux, use middleware like redux-thunk or redux-saga. With useState, use useEffect to fetch and call setTasks when data arrives. With Context, combine a custom hook and useEffect.
What about Zustand or Jotai for kanban state?
Both are excellent lightweight alternatives. Zustand is closer to Redux in philosophy (centralized store, actions); Jotai is more granular (atoms). Both have less boilerplate and better TypeScript support than Redux. For kanban, either works well if your team is comfortable with them.
Can I mix useState and Redux?
Yes. Use Redux for shared kanban state; use useState for local UI state (which column is expanded, form input focus, etc.). This keeps your store focused on domain logic.