Going to try something different this week. Let me know what you think. Doing 1 issue called The Code Review - an in-depth look at the coding progress/challenges I had over the past week.
And then I’ll do a second issue this week about the non-coding SaaS stuff. This week, it will be about Marketing 101 - building product awareness. I was going to do both in this issue, but thought it might be better to split them due to length. So let’s dig into the first version of The Code Review.
Had a great time on Saturday hanging out with the Cub Scouts, hiking, fishing, and sitting around the fire. Didn’t end up staying the night due to my lovely bronchitis, which flares up in the evening and hits the worst overnight, so I didn’t want to subject the kids (or parents) to that. But had a great day with my son and his friends.
So I came in refreshed to do some coding on Sunday and spent a few hours adding in a new feature: the ability for users to protect certain people they are following from being unfollowed. As I mentioned previously, this is actually a bigger feature than it seems at first, because I plan on tying it into the way I do things for different products in the future.
I actually had the feature mostly implemented in my last coding session, but didn’t quite get it working last time and had to spend the first half of my time today figuring out why it wasn’t working.
And it turns out, I was doing a couple of different things wrong in the frontend code, so I needed to rethink the way I did things. Here was my issue: I was trying to make two requests to the backend: one to get the dormant accounts that the user follows and one to get the accounts that were protected.
Then I was trying to update the display to show which accounts were protected by flipping the Switch component that I include in each row of the display table.
So I was getting back both responses and trying to look up each dormant account in the protected collection. Not the best approach, because there are some states that didn’t have 1 or the other. Not to mention, I had some issues with trying to update the objects when an account was protected or unprotected, so I had to untangle everything I was doing wrong in order to get it to work.
Mistake 1: Not modifying data the correct way
The first mistake I made in my initial implementation: I didn’t update the object correctly. Blitz, like NextJS, uses SWR (stale-while-revalidate). So that’s what I use to fetch data from my backend so it works the same as if I wanted to grab data from the Blitz database. I created a hook for each backend request and use that to grab the data. So let’s say I get back an array of Twitter users that are dormant.
[User 1, User 2, User 3, …, User N]
And then I get an array of Protected Users:
[User 1, User 2]
So in this example, if I want to protect User 3, I need to send a request to the backend to protect User 3, get the updated list of protected users, and then update the display to show that the user is protected. This is not an instant response, so even if it’s pretty quick, the display doesn’t update immediately. And if a user clicks it and it doesn’t immediately update, they’ll probably click it again and again and again, putting extra strain on the system and leading to a weird result.
But we know which user to add to the list of protected users, right? So why can’t we just add that user to the list?
We can, but we have to do it a certain way, and that’s what I screwed up. With the SWR hook, there’s a mutate function that you get back that you can call to let the system know that there’s an update happening to the data. So I tried to add the Twitter user object to the list, pass that in to the mutate data function as the new data, and tell it to not refetch the data from the backend.
Here’s the naive way I tried to do things:
await fetch("api/backend/protect", {
method: "POST",
credentials: "include",
headers: {
"anti-csrf": antiCSRFToken
},
body: JSON.stringify({
twitterId: twitterIdToProtect
})
})
.then((response) => response.json())
.then((json) => {
mutateProtectedAccounts(protectedAccounts.push(json), {revalidate: false})
})
.catch((error) => console.log(error))
}
So, I make the request to protect the given user, and then once I get the response back, convert it from a string to JSON, and then try to push it to the list of protectedAccounts.
There are multiple issues here, actually. First, what does push return?
I thought it returned the array. Turns out, it returns the length of the new array.
So I was very confused when I tried to protect a user and then suddenly things broke because I was trying to call array methods on a number. Took me longer than it should have to track that issue down. So I fixed it:
await fetch("api/backend/protect", {
method: "POST",
credentials: "include",
headers: {
"anti-csrf": antiCSRFToken
},
body: JSON.stringify({
twitterId: twitterIdToProtect
})
})
.then((response) => response.json())
.then((json) => {
protectedAccounts.push(json)
mutateProtectedAccounts(protectedAccounts, {revalidate: false})
})
.catch((error) => console.log(error))
}
This too had a problem, because I can’t just update the protectedAccounts object, because it’s managed by the hook. So the way I can update it is to call the mutate function. And once I realized this didn’t work, I remembered the way to implement this.
const protect = async (twitterId) => {
console.log(protectedAccounts)
await fetch("api/backend/protect", {
method: "POST",
credentials: "include",
headers: {
"anti-csrf": antiCSRFToken,
},
body: JSON.stringify({
twitterId,
}),
})
.then((response) => response.json())
.then((json) => {
mutateProtectedAccounts([...protectedAccounts, json], { revalidate: false })
})
.catch((error) => console.log(error))
}
So here, I’m calling the mutateProtectedAccounts function and passing in the new data as a new array. But to do that without including the previous array, I actually created a new array, added all of the accounts in protectedAccounts by using the spread syntax (…), and then tacked the new object (json) on the end of that array, and told it that it didn’t need to revalidate that data.
Huzzah! It worked exactly the way I wanted it to! Almost instant update to the UI and it didn’t need to then refetch all of the protectedAccounts in an additional request.
Mistake 2: Switch State
The other issue was trying to get the switch to display the correct value. I needed to pass a boolean into the switch component and I was trying to set the switch value based on what came back in protectedAccounts:
enabled={protectedAccounts && protectedAccounts.find(account => account.twitterId === twitterAccount.twitter_id)}
This isn’t the best way to do this. First of all, I have to check to see if protectedAccounts exists yet. Then I have to call find, which iterates over elements until the right one is found, if it is. That’s a pretty expensive way to do this.
So I wanted to create a map of key/value pairs that I could use to lookup values.
But what’s the issue? I need to rely on two different API calls in order to populate it. So first, I have to get the dormant accounts and populate the map with all the accounts. Then I need to get the protected accounts, and update the map for those accounts.
Took a few tries to get this setup, but figured out a really good mechanism to use: the useMemo hook.
Here’s the implementation:
const protectedLookup = useMemo(() => {
if (!protectedAccounts || !unfollows) {
return {}
}
const lookups = {}
for (const unfollow of unfollows) {
const lookup = !!protectedAccounts.find(
(account) => account.twitter_id === unfollow.twitter_id
)
lookups[unfollow.twitter_id] = lookup
}
return lookups
}, [unfollows, protectedAccounts])
So if the list of unfollows (dormant accounts, really need to get better at some of these names 😅) or the protectedAccounts is null, it just returns an empty map. Then I iterate through the list of unfollows, then see if that account is in the list of protected accounts and set the value at that key to true/false accordingly. And the cool thing is this uses the array passed in to the second parameter to determine what determines when these values change. The idea behind useMemo is to cache expensive operations when possible. So this only fires when either of those two values changes, otherwise, it just uses the cached values. This means I don’t have to run the same operation every time a part of the page re-renders, which saves a lot of potential latency.
Those were the major things I dealt with this past weekend, coding-wise. Looking at the overall product picture, I’m almost done. The one major feature I’ve got to implement is the bulk unfollowing process, but that should be pretty quick. I’m planning on just doing it as a Celery task that I can trigger to unfollow an account and then I’ll create an API endpoint that will loop through and trigger that task for everyone that was passed in. That should make it easy to handle rate limits as well, as I’ll be able to set the tasks to either wait or retry on failure. Then I’ll need to add a mechanism to notify users when all the accounts have been unfollowed. So that’s up next on the board!
Stay tuned for the second issue this week, where I'm going to walk you through my marketing plan for building product awareness!
Happy building!