You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何仅基于React实现无Bootstrap依赖的Scrollspy功能

Pure React Scrollspy (No Bootstrap/jQuery)

Got it! Let's build a Scrollspy component using nothing but React's built-in hooks and browser DOM APIs—no external dependencies needed. Here's how to do it step by step:

Core Concept

Scrollspy works by:

  • Listening to the browser's scroll event
  • Calculating the position of each content section relative to the viewport
  • Updating the active navigation link based on which section is currently in (or near) the viewport

Step-by-Step Implementation

1. Set Up Basic Structure

First, create a navigation menu and corresponding content sections. Each section needs a unique id that matches the href of its navigation link.

2. Use React Hooks for State and Refs

We'll use:

  • useState to track the currently active section ID
  • useRef to store references to our content sections (avoids repeated DOM queries)
  • useEffect to attach/detach the scroll listener and handle cleanup

3. Implement the Scroll Handler

The scroll handler will:

  • Loop through each content section
  • Use getBoundingClientRect() to get the section's position relative to the viewport
  • Determine which section is "active" (we'll define this as the section whose top is within a small offset from the viewport top)
  • Update the active state

4. Add Click-to-Scroll (Optional but Nice)

Make navigation links scroll smoothly to their corresponding sections using scrollIntoView().

Complete Code Example

import { useState, useRef, useEffect } from 'react';

const Scrollspy = () => {
  const [activeId, setActiveId] = useState('section-1');
  const sectionRefs = useRef({});
  const scrollTimeoutRef = useRef(null);

  // Register section refs as components mount
  const registerSectionRef = (id, ref) => {
    if (ref && !sectionRefs.current[id]) {
      sectionRefs.current[id] = ref;
    }
  };

  // Handle scroll event with debounce to avoid excessive re-renders
  const handleScroll = () => {
    // Debounce the scroll handler to run only after 100ms of inactivity
    if (scrollTimeoutRef.current) {
      clearTimeout(scrollTimeoutRef.current);
    }

    scrollTimeoutRef.current = setTimeout(() => {
      const sections = Object.entries(sectionRefs.current);
      let currentActiveId = activeId;

      sections.forEach(([id, ref]) => {
        const rect = ref.getBoundingClientRect();
        // Adjust this offset if you have a fixed header (e.g., 80 for a 80px tall header)
        const offset = 20;
        
        // Section is active if its top is above the offset and bottom is below the offset
        if (rect.top <= offset && rect.bottom >= offset) {
          currentActiveId = id;
        }
      });

      if (currentActiveId !== activeId) {
        setActiveId(currentActiveId);
      }
    }, 100);
  };

  // Smooth scroll to section when nav link is clicked
  const handleNavClick = (e, id) => {
    e.preventDefault();
    sectionRefs.current[id]?.scrollIntoView({ behavior: 'smooth' });
    setActiveId(id);
  };

  // Attach scroll listener on mount, clean up on unmount
  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    // Run once on mount to set initial active section
    handleScroll();

    return () => {
      window.removeEventListener('scroll', handleScroll);
      if (scrollTimeoutRef.current) {
        clearTimeout(scrollTimeoutRef.current);
      }
    };
  }, [activeId]);

  return (
    <div style={{ display: 'flex', gap: '2rem', padding: '1rem' }}>
      {/* Navigation Menu */}
      <nav style={{ position: 'sticky', top: '1rem', width: '200px' }}>
        <ul style={{ listStyle: 'none', padding: 0 }}>
          <li style={{ marginBottom: '1rem' }}>
            <a
              href="#section-1"
              onClick={(e) => handleNavClick(e, 'section-1')}
              style={{
                textDecoration: activeId === 'section-1' ? 'underline' : 'none',
                color: activeId === 'section-1' ? '#2196F3' : '#333',
                fontWeight: activeId === 'section-1' ? 'bold' : 'normal'
              }}
            >
              Section 1
            </a>
          </li>
          <li style={{ marginBottom: '1rem' }}>
            <a
              href="#section-2"
              onClick={(e) => handleNavClick(e, 'section-2')}
              style={{
                textDecoration: activeId === 'section-2' ? 'underline' : 'none',
                color: activeId === 'section-2' ? '#2196F3' : '#333',
                fontWeight: activeId === 'section-2' ? 'bold' : 'normal'
              }}
            >
              Section 2
            </a>
          </li>
          <li style={{ marginBottom: '1rem' }}>
            <a
              href="#section-3"
              onClick={(e) => handleNavClick(e, 'section-3')}
              style={{
                textDecoration: activeId === 'section-3' ? 'underline' : 'none',
                color: activeId === 'section-3' ? '#2196F3' : '#333',
                fontWeight: activeId === 'section-3' ? 'bold' : 'normal'
              }}
            >
              Section 3
            </a>
          </li>
        </ul>
      </nav>

      {/* Content Sections */}
      <div style={{ flex: 1 }}>
        <section
          id="section-1"
          ref={(ref) => registerSectionRef('section-1', ref)}
          style={{ height: '80vh', backgroundColor: '#f5f5f5', padding: '2rem', marginBottom: '2rem' }}
        >
          <h2>Section 1</h2>
          <p>Scroll down to see the active nav link update!</p>
        </section>
        <section
          id="section-2"
          ref={(ref) => registerSectionRef('section-2', ref)}
          style={{ height: '80vh', backgroundColor: '#e0e0e0', padding: '2rem', marginBottom: '2rem' }}
        >
          <h2>Section 2</h2>
          <p>Keep scrolling...</p>
        </section>
        <section
          id="section-3"
          ref={(ref) => registerSectionRef('section-3', ref)}
          style={{ height: '80vh', backgroundColor: '#bdbdbd', padding: '2rem' }}
        >
          <h2>Section 3</h2>
          <p>You've reached the bottom!</p>
        </section>
      </div>
    </div>
  );
};

export default Scrollspy;

Key Optimizations & Customizations

  • Debouncing: The scroll handler uses debouncing to limit how often it runs (100ms delay), which prevents performance issues from frequent scroll events.
  • Offset Adjustment: If you have a fixed header, tweak the offset value in the scroll handler to account for its height (e.g., set to 80 for an 80px tall header).
  • Smooth Scrolling: The handleNavClick function uses scrollIntoView({ behavior: 'smooth' }) for smooth scrolling—this is supported in all modern browsers.
  • Reusability: You can refactor this into a reusable hook (e.g., useScrollspy) that accepts section IDs and returns the active ID, making it easy to integrate into different components.

Notes

  • This implementation works for vertical scroll (the most common use case). For horizontal scroll, you'd adjust the logic to check left and right instead of top and bottom.
  • If you're using Server-Side Rendering (SSR), wrap the scroll listener setup in a check for typeof window !== 'undefined' to avoid errors during server rendering.

内容的提问来源于stack exchange,提问作者leuction

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 09:54:51