The clock on my monitor silently ticked, while I cried in JSX fragments and Spring beans. It was Week 15 and 16- the final stretch of my coding bootcamp and we were tasked with creating a full-stack app. Time was tight from having to research and execute new concepts, and the bugs- oh someone call pest control! š
Though it was tough, I did end up finishing and learned a bunch in the process. My project was a volunteer-driven maps application that allows users to crowdsource accessibility information as well as explore and search accessible places near them.
While my Github readme shares more in-depth details of my learnings, I wanted to also share a behind the scenes view of 4 bugs I faced and how I debugged them:
Table of Contents
1. Duplicate Data Seeding
Issue: When seeding data without using a list, duplicate entries were created.
Solution: Created a variable to store the data seeder return and referenced this variable in other functions. This prevented duplication when checked in Postman.
To ensure the app had places loaded along with user reviews (to simulate a database of Maps places) and feature tags, a data seeding mechanism was used. š±. My approach at the beginning was to call the return of the entity seeder class. For example, in my reviewSeeder
, I called userSeeder.seedUsers()
, thinking that would just get me the return value of the initial list of 10 seeded users. Instead, I ended up with duplicatesāPostman showed 20 users instead of 10 š¬. When checking that list, I noticed that the usernames repeated twice, i.e. user1
appeared twice with id
of 1 and 11.
After an hours-long trip down a rabbit hole, I realized that userSeeder.seedUsers()
appeared to invoke the seeder function again instead of just returning the initial seeded list.
To fix this, I created a variable to hold the data seeder's return value and referenced this variable in subsequent functions. This change effectively prevented duplication, confirmed by rechecking in Postman.
public void run(String... args) throws Exception {
List<User> seededUsers= userSeeder.seedUsers();
// Seed users first
List<FeatureTag> seededTags = tagSeeder.seedTags(); // Then, seed tags first
List<Place> seededPlaces = placeSeeder.seedPlaces(seededTags); // Pass seeded tags to places
reviewSeeder.seedReviews(seededPlaces, seededUsers); // Pass seeded places and tags to reviews (because reviews can only exist with a place and places have tags)
}
2. Rating Button Not Showing as Checked
Issue: **The rating button wasnāt properly working or showing as checked upon user selection.
**Solution: Used the checked attribute to control the selected radio button based on the component's state, as well as corrected mapping logic.
When adding a review, users are also prompted to rate the accessibility of the place from 1-5. Getting the rating buttons to display as checked was another challenge ā.
Kudos to my instructor who worked through with me during office hours.
Firstly, we fixed the mapping logic. The way rating radio buttons are generated is to take the array of ratings [1, 2, 3,4,5] and then map through them.
{[1, 2, 3, 4, 5].map((value) => { ... })}
A map
typically takes 2 parameters: the value (current element of the array being processed and the index of the current element in the array. In this case, (value) => { ... }
is an anonymous function that takes value as its parameter. and it is saying for each rating number, we want to have it be a radio button.
{[1, 2, 3, 4, 5].map((value) => {
return ( // return a radio button for each number in the array
<Form.Check
key={value}
type="radio"
label={value} // set label text for radio button
name="rating"
value={value} // set value attribute to the current value we are mapping over
checked={formData.rating === value.toString()}
/>
)
After that, I was able to confirm that yes, the value of the userās selection was read by using a console.log
and confirming the value was read on click. However, the button still appeared as unchecked, and that can be confusing to the end user.
We researched and learned that the checked
attribute is what helps React determine which button to select when the form renders. This meant that there was an issue with how we were defining the statement in checked.
The values in the bracket had to evaluate to true and we were comparing formData.rating
to value
(which was the value of the radio button generated from our mapping).
We confirmed that this comparison had to evaluate to true as we wrote checked = {false}
; the formData.rating value was read on the console, but the button was not checked - which proves that when the comparison is false
, a check will not appear visually in our UI.
Therefore, we dug a bit further into how we were getting those values and comparing them.
formData.rating is set using the handleChange
which sets the rating value when the user clicks on a radio button. (Essentially, the function looks at event.target.name
aka fields that triggered the change, and gets its value and sets it to form data.
const handleChange = (event) => {
const { name, value } = event.target; // destructures the event.target with the keys in brackets
// this way, we can use `name` and `value` variables vs `event.target.name, event.target.value
setFormData((prevFormData) => ({
...prevFormData, //takes the form data and makes copy of it
[name]: value, //gets value for fields that triggered the change and sets it to form data.
}));
};
We ran a console.log
to compare formData.rating
and value.
In the end, we saw the issue was a type mismatch. After researching and seeing a suggested toString
method online, we used that with our own code. formData.rating === value.toString()
generated true
and the check was now appearing on the UI. ā
We could also verify this with the console.log
. You can see when the user clicks 2.
Line 88 is formData.rating
and Line 89 is value.toString()
. You can see 5 lines appear - which is from our mapping of the 5 ratings, and for each it checks to see if the value
we are mapping over from the array is equal to the userās selection. When it is mapped over 2, that matches what the user selected, so the check appears visually in the UI.
3. Uncontrolled Component Warning
Issue: Input fields were locked because they were directly bound to userData
.
Solution: Made a copy of userData
to allow edits and saved changes on submit. This prevented the form from locking while enabling updates.
When designing the Edit Account
page, I wanted data from My Account
(which was retrieved from a GET
mapping call to also populate on Edit Account
). I used the UserData
context provider in React to carry over those values. While the information did port over correctly to Edit Account
, the input fields were locked š, preventing edits.
Shoutout to my mentor who helped me to battle this bug. The console showed an error of āuncontrolled component warning.ā We learned this error is when the state of a component is not being controlled by React itself, aka React doesnāt have complete control over the Edit Account
formās input fields. Yes, React is a control freak. š
Fields were directly bound to userData
(which was set from that aforementioned API call on My Account
). This resulted in the fields being "locked" and preventing any edits. This also means that when I was trying to edit the input fields, I was essentially trying to edit the original userData. React doesn't allow direct changes to props because they are supposed to be immutable. So, trying to edit the input fields directly would essentially be trying to modify immutable data, which React won't allow.
Also, when an input field is directly bound to a piece of data- in this case userData
, React cannot fully control the state of that input field.
We resolved it by creating a copy of userData
, allowing modifications without altering the original until submission.
const [formData, setFormData] = useState({ ...userData });
formData
is a variable that refers to that copy of userData
(with its key-value pairs of data details), so in our form fields, we can use the dot notation of value={formData.email}
.
This also fixed the uncontrolled component warning, ensuring form fields were populated with the initial userData
values but remained editable. Upon submitting, changes were saved back to the original user data with a PUT
request, ensuring a smooth and functional user experience.
Finally, the user is redirected back to My Account
after a successful PUT
call, and that is where GET
mapping happens to retrieve the user info and set it to the userData
context provider- ensuring both the backend and the frontend context providersā values are updated š¾.
4. Conditional API Calls
Issue: Fetch API calls wouldnāt populate with data on the frontend
Solution: Ensured API call was made only if the username
was truthy, triggering the call once the username was available.
A peculiar issue with populating data from fetch API arose š§. The first time I noticed this was when trying to get My Account
details to populate by username. My API call required the username
value.
const responseData = await fetchData(`users?username=${username}`);
I started with using local storage to store the username upon a successful sign in, but the API call to get account details would not return anything. I even did a console.log
to ensure the username was being correctly read. The instructor gave a hint on how local storage can be slow to load. Thus, I then tested using a username
context provider to pass the value - thinking this would resolve it. Still, there was no luck in rendering the API callās return.
To prove that it was not an issue with the system reading the username
value, I even tried to hardcode a username
value of const username = user1
before the API call, and that worked. Something else was brewing.
As I initially had an address
entity linking to user
, I tried to change the dot notation format to users.address
, users.
, address
, etc- all to no avail either. I then thought perhaps the address
entity was giving me issues due to how it was set up on Spring Boot with the one-to-one cascade, so I commented it out to see if I could at least get the user
information to populate. It did!
When I uncommented out address
, then the address
would populate. I tested this a few times with mixed results, and noticed I had to wait a bit to uncomment out address
for both the user and address to display. This gave me another hint, that perhaps we had to wait for the user information to populate.
A few hours later, what I learned is that given that API calls are asynchronous, there was a possibility that the data might not be available at the time of the call. Also, local storage and context providers are asynchronous too. That means JavaScript won't wait for the local storage or context operation to finish before continuing to execute the API calls.
That means that when we attempted to fetch "My Account" details based on the username
, there was no guarantee that the username would be available immediately. It could take some time for the local storage to be accessed and the username to be retrieved.
By implementing a conditional API call that triggered only if the username was truthy, I ensured the address field was populated correctly. This method checked if the username was available before making the API call, allowing the address data to load appropriately. This method highlighted the importance of conditional logic in ensuring seamless data fetching and rendering in the UI š
useEffect(() => {
if (username) {
fetchUserData(username);
}
}, [username]);
Top comments (0)