Virtual Scrolling Architecture in React.js — Why Tables Lose Data and How to Maintain Visual State
♻️ Knowledge seeks community 🫶 #12
Hey hey! 👋
Welcome back to another article on Stef’s Dev Notes!
Before we dive into today’s topic, I just want to take a moment to thank you for being here. ❤️ We’ve been slowly growing this little piece of newsletter, and I honestly couldn’t be happier about it.
With our first-year anniversary coming up, I’m especially proud that I kept going. Sure, I took breaks when I felt overwhelmed — but I always found my way back. And it’s because of you — your support, your interactions, and your encouragement — that this newsletter continues to grow.
Regarding today’s topic, we will go a bit more technical this time.
Last week, I ran into an interesting bug.
While I can’t give a lot of context about it, I will frame it like this:
Imagine you’re working with a large table — about 1,000 rows or even more — and you edit a cell in one of those rows. Then you scroll away to check the values in the same column for other rows. When you scroll back up to the row you just edited… surprise! The data in that cell has disappeared.
The funny part? The “Save” button at the top of the table was still active — so my data was clearly there somewhere, just not visible!
That experience led me to write this article about virtual scrolling architecture in React.js and how to manage visual state correctly. We'll explore why this happens, the core principle for fixing it, and several practical strategies you can implement right away.
The Virtual Scrolling Dilemma
This frustrating experience perfectly illustrates a fundamental challenge with virtual scrolling architecture.
Virtual scrolling is essential for performance with large datasets. Instead of rendering 1,000+ DOM elements, we might only render 20-30 visible items plus a few buffer items. As users scroll, components are dynamically created and destroyed.
However, this optimization creates several challenges:
Edited form values disappear when scrolled out of view
Selection states reset unexpectedly
Expanded/collapsed rows lose their state
Hover effects and focus behave inconsistently
Custom visual modifications vanish
The fundamental issue is that we're storing state in components that have a temporary lifecycle, while expecting that state to have a permanent lifecycle.
A Broken Example
Here's what typically goes wrong:
Why Does This Happen?
Understanding the root cause is crucial for implementing the right solution.
When using virtualization, the components that represent individual items don't live forever. They mount and unmount as they enter and leave the viewport. Here's what happens:
User scrolls down → Row component unmounts →
useState
values are lostUser scrolls back up → New row component mounts → State initializes to default values
Previous edits disappear → User confusion ensues
This is fundamentally different from non-virtualized lists where each row component stays mounted throughout the user session. With virtualization, the UI acts like a "sliding window" over your data—components are constantly being recycled.
Think of it like looking through a telescope: you're only seeing a small portion of a much larger landscape, and that view changes as you move the telescope around.
The Core Principle: Decouple State From Visual Representation
The key to fixing these kinds of bugs is simple in theory but requires discipline in practice:
Never put long-lived state in short-lived components.
Your virtualized row should be a "pure view," derived entirely from some persistent data source—not its own local useState
. All edited data, UI states, and user interactions must live outside the visual component, so when the component is destroyed and recreated, the data is restored correctly.
Strategies for Managing Visual State
Here are some practical ways to achieve this:
1. Lift State Up
Keep the source of truth for each row’s data in a stable structure — like a Redux store, Zustand store, or even a top-level useState
.
Pros: Simple, predictable, works well with React DevTools
Cons: Can cause performance issues with very large datasets due to re-renders
2. Use a Keyed Cache (Best for Large Datasets)
If your dataset is too large to hold entirely in React state, use a Map
or similar structure to cache only the modified values.
Pros: Memory efficient, fast lookups, scales well
Cons: Requires manual cache management, potential memory leaks if not cleaned up
3. Sync Form State Strategically (Hybrid Approach)
Allow temporary local state for smooth UX, but sync to stable storage at strategic moments.
Pros: Best user experience, responsive interactions
Cons: More complex, requires careful event handling
4. External State Management Libraries
For complex applications, consider dedicated state management:
Redux Toolkit:
Zustand (lighter weight):
Performance Considerations
When lifting all this state up, it’s tempting to re-render your whole list.
That’s where techniques like:
React.memo on row components
Selective updates based on props
Virtualized lists like
react-window
orreact-virtualized
come into play. Only the visible items will re-render, so it stays clean.
Debugging and Testing
Common Debugging Steps
Check component lifecycle: Add
console.log
inuseEffect
to see mounting/unmountingVerify state location: Ensure critical state isn't in the virtualized component
Test edge cases: Rapid scrolling, large datasets, simultaneous edits
Testing Strategies
Error Handling and Edge Cases
Cache Size Management
Network Failure Recovery
Quick Reference Checklist
When implementing virtual scrolling with state management:
Identify persistent state: What needs to survive component unmounting?
Choose storage strategy: Lifted state, cache, or external store?
Implement error boundaries: Handle edge cases gracefully
Add performance optimizations: Memoization, batching, debouncing
Test thoroughly: Rapid scrolling, large datasets, edge cases
Monitor memory usage: Especially with caching strategies
Document state flow: For future maintainers
Wrapping up
Virtual scrolling is an amazing optimization for big lists—but it requires careful attention to where you store your state. The key insight is treating your visual components as pure "windows" into your data rather than independent entities with their own lifecycle.
Remember: short-lived components should never hold long-lived state. Follow this principle, choose the right strategy for your use case, and you'll avoid those disappearing cells and other weird bugs that can frustrate your users.
The strategies we've covered will handle most scenarios you'll encounter. Start with the simplest approach that meets your needs, and don't be afraid to refactor as your requirements grow.
Until next time,
— Stefania
Articles from the ♻️ Knowledge seeks community 🫶 collection: https://stefsdevnotes.substack.com/t/knowledgeseekscommunity
Articles from the ✨ Frontend Shorts collection:
https://stefsdevnotes.substack.com/t/frontendshorts
👋 Get in touch
Feel free to reach out to me, here, on Substack or on LinkedIn.