From Flickering Icons to Smooth Scrolling: A Developer’s Journey

Website Updates: Theme, Disclaimer, and Employment

I’ve recently updated the website’s theme, disclaimer, and employment pages. The employment section is growing, which is great because it allows me to have a comprehensive overview of my work history. Most resumes I provide only show the last seven years, so having a more detailed record here is beneficial.

I’m still undecided about whether to include volunteer roles in my employment history. Serving on the board of zoning appeals felt like a volunteer position, even though I received a small stipend for attending meetings.

Prettify TypeScript: Better Type Previews

I’ve recently installed the Prettify TS extension for Visual Studio Code. This extension is a game-changer when working with complex TypeScript types.

By default, when you hover over a type in TypeScript, you’ll see a list of other types that compose its structure. This can be helpful for understanding basic objects, but it can become overwhelming when dealing with more intricate types.

Prettify TS addresses this issue by providing a breakdown of the type into its fundamental primitive types, making it resemble a typed JSON object. While it still displays the original type structure below, this visual representation offers a clearer understanding of the type’s composition.

Here’s a before-and-after screenshot to illustrate the difference:

TypeScript
TypeScript + Prettify TS

Issues Encountered with the Icon Picker

Microsoft Designer: Image Creator Prompt

Create a colorful icon picker component with various icons, a search bar, and filters, mockup on a smartphone screen. This image should be visually appealing and clearly demonstrate the functionality of an icon picker component. The colors should be vibrant and eye-catching, and the layout should be clean and organized.

While navigating the icon picker, I’ve noticed several issues:

  • Icon Disappearance: Icons occasionally disappear while more are being fetched.
  • Invalid Offset: The URL sometimes includes an invalid offset=NaN parameter when scrolling past the end of a small result set.
  • Excessive Server Requests: The web server receives numerous requests when scrolling or typing, even with useDeferredValue. Throttling might be necessary to reduce the load.
  • Clipped Tabs: The edge of the last tab is not visible. Clicking it scrolls it into view, but the first tab becomes clipped.
  • “Other” Tag Persistence: The “Other” tag is still present, even though it was previously reserved for uncategorized icons.
  • Improved Messaging: More informative messages are needed for loading, no matches, and error states.
  • Editing Capabilities: A way to edit icon tags, filters, slugs, and labels is required.

Resolving the “NaN” Issue

icons?filters=filled&limit=96&offset=NaN&search=bat&tags=device

I believe the issue lies in a division by zero. We should prevent the request from being made if the offset is greater than the total, unless the total is unknown.

After further investigation, I discovered the root cause of the “NaN” error. The calculation for the total scrollable height was incorrect. It subtracted the visible page height, which resulted in zero when there was only one page of results. This zero value was then used as a divisor, leading to infinity. The subsequent multiplication of infinity by zero produced the “NaN” value.

Here’s the corrected code:

// This returns zero
const scrollableHeight = totalHeight - pageHeight;
// Infinity * 0 is NaN
const visibleTopRow = Math.floor(
    (deferredScrollTop / scrollableHeight) * scrollableRows
  );
// Which propagates NaN here
const firstDataRow = visibleTopRow - hiddenRowsBefore;
// And NaN is greater than zero
const offset = Math.max(0, firstDataRow * visibleColumns);

// This is the fix
const scrollableHeight = Math.max(pageHeight, totalHeight - pageHeight);

Optimizing Component Rendering for Performance

To improve the performance of the icon picker, I believe we need to restructure the component into three groups of child components. This will allow us to keep two groups loaded while fetching data for the third group that is currently out of view.

While this change will slightly alter the component’s layout, the overall impact should be minimal. Here’s a breakdown of the necessary steps:

  1. Render Multiple Child Components: Render multiple child components, each with a different offset in the context.
  2. Implement Stepping: Load data one page at a time instead of a fixed number of rows. This will reduce the number of REST API calls by 75% for a four-row-per-page configuration.
  3. Adjust Calculations: Modify the calculations for the three controls to accommodate this new approach.
  4. Avoid Rendering Out-of-Range Components: Don’t render child components that are outside the visible range.

The first step is to create an array of offsets based on the existing offset, limit, and totalItems values. This array will define the starting points for each page of data.

const getOffsets = ({
  offset, // starting offset value
  pageCount, // total number of pages to request
  itemsPerPage, // number of items per page
  totalItems, // total items count (or -1 if unknown)
  TOTAL_UNKNOWN = -1 // constant representing unknown totalItems
}: {
  offset: number,
  pageCount: number,
  itemsPerPage: number,
  totalItems: number,
  TOTAL_UNKNOWN?: number
}): number[] => new Array(pageCount)
  // Populate with positive offset, or zero
  .fill(Math.max(0, offset))
  // make sure we don't have "Not a Number"
  .filter(offset => !isNaN(offset))
  // Snap offsets to first item of each page
  .map<number>(offset => offset - (offset % itemsPerPage))
  // Each page starts where the last one left off
  .map((offset, index) => offset + (itemsPerPage * index))
  // Offset is zero
  // Or less than total items (if known)
  // Or total items is unknown (-1)
  .filter((offset) =>
    offset === 0 || offset < totalItems || totalItems === TOTAL_UNKNOWN
  );

I believe this approach will provide a solid foundation. The code effectively calculates offsets for three virtual pages, ensuring they align with page boundaries, are not “NaN,” and are valid based on the total number of items. The flexibility to adjust the number of pages from 3 to 5 is a valuable feature.

To render the child components for each page, we can simply reuse them with different offset values in the context. This will allow us to efficiently display the data for each virtual page.

{offsets.map(offset =>
  <context.Provider key={offset} value={{
    offset, 
    limit: itemsPerPage, 
    setTotalItems, 
    dependencies
  }}>{children}</context.Provider>
)}
Optimizing Performance with Memoization

To prevent unnecessary re-renders of the page, we need to memoize the array of offsets and the context value. Memoization is a technique that caches the result of a function call and returns the cached value for subsequent calls with the same arguments.

By memoizing these values, we can avoid redundant calculations and improve the overall performance of our component.

const pages = useMemo(() => getOffsets({ 
    offset, 
    pageCount: 3, 
    itemsPerPage, 
    totalItems, 
    TOTAL_UNKNOWN 
  }).map(offset => ({
    offset,
    limit: itemsPerPage,
    setTotalItems,
    dependencies
  })),
[ 
  offset, 
  itemsPerPage, 
  totalItems, 
  dependencies, 
  setTotalItems
]);

This approach appears to be effective. The implementation is now significantly more concise and easier to understand.

{pages.map(page =>
  <context.Provider
    key={page.offset}
    value={page}
  >
    {children}
  </context.Provider>
)}
Addressing Unexpected Challenges

The implementation was initially straightforward, but I encountered several unforeseen challenges. After delving into the code, I successfully resolved these issues.

The grid is now functioning as expected, and I no longer experience the intermittent flickering that occurred when loading data row by row. Loading data in separate pages seems to be the most effective approach.

The boundary between loaded and fetching data is more apparent now that pages are loaded as individual child components. Instead of displaying a loading message, I opted to use the MUI Skeleton component to visualize the content layout with a subtle pulsating animation, indicating that data is being fetched.

I encountered some difficulties with keys in arrays. To troubleshoot, I created an array of placeholder icons and assigned IDs based on their position within the array. However, I discovered that these IDs were not unique, which was unexpected.

Despite the issue, I decided to use the IDs in the final implementation, but I applied them during the content rendering process to ensure uniqueness.

// Duplicate Id's
icons = new Array(limit)
  .fill(offset)
  .map(offset => ({ id: offset + index }));

// Good
{icons.map((icon, index) => {
  return (<Grid key={offset + index}

Testing with Throttled Network Speeds

To simulate slower network conditions, I’ve throttled my network speed to 1 kilobit per second (kbps) in Google Chrome. This helps me test how the application performs under less ideal network conditions.

I would love to experience even slower speeds, like 600 bps or 300 bps, to relive the early 80’s dial-up era and the speed of my old Atari 400 cassette drive. Here are the Chrome settings I used to throttle the network speed:

Icon Picker Development: Outstanding Issues

It’s getting late, so I’m documenting the remaining issues with the icon picker for future reference:

Scrolling Problems:

  • Scrolling to the top after selecting an icon on a scrolled-down page.
  • Inability to scroll down after switching to an uncached page.
  • Potential issue with totalItems not updating when tabbing between styles.

Visual Inconsistencies:

  • Text portion of loading icons displaying as a thin line.
  • Truncated text overlapping into adjacent cells.

Functionality Issues:

  • Debouncing API requests for typing to avoid excessive calls.
  • Pre-populating total matching records for the infinite grid using available tab counts.
  • Implementing edit capabilities for icons.
  • Refining the message displayed for no matching icons.

Code Improvements:

  • Removing the unnecessary “Other” tag.
  • Addressing the DOM nesting error related to the icon picker’s placement.
  • Refactoring the component into a slice for better organization.
  • Reducing the data returned by search (exclude icon slug and setId slug).
  • Separating the filters into a dedicated user control.
  • Moving the icon picker dialog to a dedicated slice.

Integration Tasks:

  • Configuring API endpoints for editing functionality.
  • Integrating the Emoji library.
  • Integrating the Material Symbols library.

I’ll continue working on these items to finalize the icon picker component.

Discover more from Lewis Moten

Subscribe now to keep reading and get access to the full archive.

Continue reading