I recently hit one of those frustrating bugs that feels obvious in hindsight but had me scratching my head at first. While building a small project with Spring Boot, MySQL, and React, I wanted to display a user’s expenses in a table. Everything compiled fine, but as soon as I opened the page.
Uncaught TypeError: this.state.Expenses.map is not a function
Sound familiar, Let’s walk through what happened, why it happens, and how I fixed it. Then I’ll share how I leveled up the project with some extra functionality like search, sort, totals, loading states, and delete.
The Code I Started With
Here’s what my original setup looked like.
React (Class Component)
componentDidMount(){
const currentUser = AuthService.getCurrentUser();
let userId = {...this.state.userId};
userId = currentUser.id;
this.setState({userId});
UserService.getExpense(userId).then((res) =>{
this.setState({Expenses: res.data});
});
}
</thead>
<tbody>
{
this.state.Expenses.map(
(expense, key) => {
return (
<tr key = {key}>
<td> {expense.title} </td>
<td><Moment date={expense.date} format="YYYY/MM/DD"/></td>
<td> {expense.category}</td>
<td> {expense.amount}</td>
<td>
<button onClick={ () => this.deleteExpense(expense.id)} className="btn btn-danger">Delete </button>
</td>
</tr>
)
}
)
}
</tbody>
</table>
Axios Function
const API_URL_EX = 'http://localhost:8080/api/test/expense';
getExpense(userId){
return axios.get(API_URL_EX + '/' + userId);
}
Spring Boot Controller
@GetMapping("/expense/{userId}")
public ResponseEntity<?> getExpense(@PathVariable Long userId){
Optional<Expense> expense = expenseRepository.findByUserId(userId);
return expense.map(response -> ResponseEntity.ok().body(response))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
Sample Database Row
# id, amount, category, date, title, user_id
'1', '50000', 'Shopping', '2022-11-06 11:21:23', 'Dubai Trip', '1'
And then the error:
viewexpense.component.js:68 Uncaught TypeError: this.state.Expenses.map is not a function
Why that Error Occur
The key is in the phrase:
.map
only exists on arrays.
React was trying to loop over this.state.Expenses
as if it were an array. But my backend controller wasn’t sending an array. I used Optional<Expense>
and findByUserId
, which only returns a single expense object. That means React got an object like:
{"id":1,"title":"Dubai Trip","date":"2022-11-06T11:21:23","category":"Shopping","amount":50000}
Not an array. So when React ran this.state.Expenses.map(...)
, it crashed.
Two Correct Ways to Fix It
(Recommended): Return a List From the Backend
If a user can have multiple expenses, it makes sense to always return a list.
Repository
public interface ExpenseRepository extends JpaRepository<Expense, Long> {
List<Expense> findAllByUserId(Long userId);
}
Controller
@GetMapping("/expense/{userId}")
public ResponseEntity<List<Expense>> getExpenses(@PathVariable Long userId){
List<Expense> expenses = expenseRepository.findAllByUserId(userId);
return ResponseEntity.ok(expenses); // returns a JSON array
}
Now React receives res.data
as an array, and .map
works.
Wrap the Single Object in an Array on the Client
If changing the backend isn’t an option, I can adapt the frontend:
UserService.getExpense(userId).then((res) => {
const data = res.data;
const Expenses = Array.isArray(data) ? data : (data ? [data] : []);
this.setState({ Expenses });
});
And I also initialize state as an array:
state = { Expenses: [] };
Both options fix the error, but Option A is cleaner.
Practice Upgrade: a Modern React Version
Once I solved the error, I decided to take the chance to refactor into a functional component with hooks. I added:
API Service
// services/expenseService.js
import axios from "axios";
const API_URL_EX = "http://localhost:8080/api/test/expense";
export const getExpenses = (userId) => axios.get(`${API_URL_EX}/${userId}`);
export const deleteExpense = (id) => axios.delete(`${API_URL_EX}/item/${id}`);
Spring Boot Delete Endpoint
@DeleteMapping("/expense/item/{id}")
public ResponseEntity<Void> deleteExpense(@PathVariable Long id) {
if (!expenseRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
expenseRepository.deleteById(id);
return ResponseEntity.noContent().build();
}
React Component
import React, { useEffect, useMemo, useState } from "react";
import Moment from "react-moment";
import { getExpenses, deleteExpense } from "./services/expenseService";
import AuthService from "./services/authService";
export default function ExpenseTable() {
const [expenses, setExpenses] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [query, setQuery] = useState("");
const [sortKey, setSortKey] = useState("date");
const [sortDir, setSortDir] = useState("desc");
useEffect(() => {
const { id } = AuthService.getCurrentUser();
getExpenses(id)
.then((res) => setExpenses(Array.isArray(res.data) ? res.data : []))
.catch((e) => setError("Failed to load expenses"))
.finally(() => setLoading(false));
}, []);
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
const list = q
? expenses.filter((e) =>
Guardrail for The Future
- Always initialize arrays as arrays:
const [items, setItems] = useState([]);
- Validate API shapes before using
.map
. - Render defensively with
Array.isArray
. - Match API contract with UI: if the UI expects a list, make sure the backend returns one.
Final Thought
In my case, the error boiled down to this: Expenses
was not an array. The cleanest fix was making the backend return a list of expenses. But if that’s not possible, you can always wrap the object into an array on the client. Once that was solved, I enhanced the project with features that made it feel like a real-world app search, sort, totals, delete, and proper loading states.