Contact

Currently working on Music Discovery

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

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.

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

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