Speechify First Round Recap
I wrote this after the first round of my Speechify interview to reflect on areas of improvement with my assessnent. This is currently only intended to be read by the Speechify team to help me understand if there’s anything I missed in this review or if there are extra areas to address. Thank you in advance for reading.
Behaviorial Areas of Improvement
- I didn’t take enough time to understand the codebase and know what tools and types were available to me
- I rushed to implement the
Intersection Observerwithout giving myself to understand how it works, according to the MDN docs - I tweaked values randomly without properly debugging my progress as I went along, to fully understand what data I was dealing with
- I was targeting the last element in the list using CSS, which wasted ≈45mins
Approach
My initial approach
The APIs that came in mind when I faced this was to either use a scroll event listener, or the Intersection Observer. Since the action was only going to be taken at the bottom of the list, there was no need to continually listen for scroll events, hence my decision to go with the latter option.
My thought process took two major steps.
Observing the scrollable container
The first was to observe scrollable, triggering at the end of the list. It took me a while to understand that observer.observe() was to be used by targeting a specific element in the list, ideally the last one. This is something I could have avoided if I didn’t rush through the documentation.
Observing the last item
The second part was spent thinking about targeting the final element of the list. I figured, since we are going to be adding more elements to the list, having to append an identifier to the last element in the list can first be validated by using className
const lastPost = document.querySelector('.last-post');
if (lastPost) {
observer.observe(lastPost);
}
...
{posts.map((post, index) => (
<PostCard
key={post.id}
post={post}
className={index === lastPostIndex ? 'last-post' : ''}
/>
))}
I could have used the use ref as well. Checking in the map and assigning the rep to the final elements for reference (Right method, wrong approach).
...
const observer = new IntersectionObserver(callback);
if (lastPostRef.current) {
observer.observe(lastPostRef.current);
}
...
{posts.map((post, index) => (
<PostCard
key={post.id}
post={post}
ref={index === lastPostIndex ? lastPostRef : null}
/>
))}
Since we can use the CSS to target the last post type with . So in the list we can target the last of type of post: root: document.querySelector(".post-list")
A better approach
Thinking about the UI design of lists, they tend to have some padding at the bottom so the last element doesn’t lie so close to the bottom of the screen/container. Treating that padding as an invisible element to be observed seems to be a good approach since I don’t have to reassign values based on the length of the list or target with CSS.
I have come to learn that this is called a Sentinel Element Pattern.
// Add a sentinel ref
const sentinelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!sentinelRef.current || isFetchingPosts) return;
const observer = new IntersectionObserver(
(entries) => {
const sentinel = entries[0];
if (sentinel.isIntersecting) {
getPosts(currentPage);
}
},
{
root: document.querySelector(".post-list"),
rootMargin: "0px", // To be improved
threshold: 1.0, // To be improved
}
);
observer.observe(sentinelRef.current);
// Cleanup observer
return () => observer.disconnect();
}, [currentPage, isFetchingPosts]);
...
{/* Sentinel element - always at the bottom */}
<div
ref={sentinelRef}
className="sentinel" // height and backgroundColor styling
/>
Improving the Intersection Observer
Having the user see the last post in the list before loading more can be interpreted in a number of ways.
- The user’s internet could be too slow to load more posts before they go to the last one.
- The app might not be fetching new posts early enough.
Bringing UX considerations into light. It would probably be better to call fetchPosts before we get to the sentinel element. In order to do this, we would need a better understanding of how the observer works. Going back to the docs and reading without rush, here’s what I found:
Root Element
The element that is used as the viewport for checking visibility of the target. Must be the ancestor of the target. Defaults to the browser viewport if not specified or if null
.post-list is used as the boundary.
RootMargin Behavior
Margin around the root… The values can only be in pixels or percentages. This set of values serves to grow or shrink each side of the root element’s bounding box before computing intersections
rootMargin: "300px" would mean intersection detection will happen 300px before getting to the element.
Threshold Explanation
“Either a single number or an array of numbers which indicate at what percentage of the target’s visibility the observer’s callback should be executed.”
So threshold: 0.1 would cause intersection to trigger when just 10% of your target element is
visible within the observed root
This led me to setting the options to:
root: document.querySelector(".post-list"), // Considering 80vh height
rootMargin: "200px", // Trigger earlier for smoother experience
threshold: 1 // Doesn't really matter since the sentinel is 1px
Other Improvements
PostCardsnow display actual names instead of user IDs, with graceful fallback to “Mystery person” if data unavailable 😄fetchUsers()function to retrieve user data from JSONPlaceholder/usersendpoint- Users stored in
Map<number, User>for O(1) lookup, fetched once on mount withgetAuthorName(userId)helper function ErrorStatecomponent with styled error messages and retry button- User-friendly error messages instead of just console logs
- Proper cleanup with observer.disconnect() to prevent memory leaks
What I learned
I have to admit that being put under observation like that is something I have experience in doing. The pressure caused me to rush, which impaired my problem-solving skills. I see now that would need developing. Thank you to Speechify for helping me grow.
Thank you for reading this. I hope it’s pleasantly sunny where you are 👋🏽
Full Code here: Secret Gist