首页 > 解决方案 > 试图在窗口上添加滚动事件侦听器,但得到 Uncaught TypeError: Cannot read property 'classList' of null

问题描述

我正在尝试将底部阴影添加到 react js 中滚动时的搜索栏。在我进入应用程序的第二页之前,它运行良好。

当我试图进入第二页时,它显示

未捕获的类型错误:无法读取 null 的属性“classList”

不工作的代码:

import React, { useEffect } from 'react';
import { AiOutlineSearch } from "react-icons/ai";

const SearchBar = ({ totalPrograms, programs, setPrograms }) => {

    const handleScroll = () => {
        if(window.scrollY) {
            document.getElementById('sb-header').classList.add('h-shadow');
        }
        else {
            document.getElementById('sb-header').classList.remove('h-shadow');
        }
    }

    useEffect(() => {
        window.addEventListener('scroll', handleScroll);

        return () => {window.removeEventListener('scroll', handleScroll)};
    },[]);

    const handleSubmit = (event) => {
        event.preventDefault();

        const searchProgramName = document.getElementById('search').value;
        
        if(searchProgramName) {
            setPrograms(
                programs.filter(program => 
                    program.name.toLowerCase().includes(searchProgramName)
                )
            );
        }
        else {
            handleClick();
        }
    }

    const handleClick = () => {
        const allPhase = document.getElementsByName('phase');
        const checkedPhaseValue = [];

        allPhase.forEach(phase => {
            if(phase.checked) {
                checkedPhaseValue.push(phase.value);
            }
        });

        setPrograms(checkedPhaseValue.length ?
            totalPrograms.filter(
                program => checkedPhaseValue.includes(program.phase.toLowerCase())
            )
            : totalPrograms
        );
    }

    return (
        <header id='sb-header' className="container header-sb"> 
            <form className="container container-center" onSubmit={handleSubmit}>
                <div className="type-search">
                    <AiOutlineSearch className="icon"/>
                    <input id="search" type="search" placeholder="search by program name"/>
                </div>
                <div className="checkbox-container">
                    <div className="d-inline">
                        <input type="checkbox" onClick={handleClick} name="phase" id="open_application" value="application_open"/>
                        <label htmlFor="open_application">Open Application</label>
                    </div>
                    <div className="d-inline">
                        <input type="checkbox" onClick={handleClick} name="phase" id="application_in_review" value="application_review"/>
                        <label htmlFor="application_in_review">Application in Review</label>
                    </div>
                    <div className="d-inline">
                        <input type="checkbox" onClick={handleClick} name="phase" id="in_progress" value="in_progress"/>
                        <label htmlFor="in_progress">in Progress</label>
                    </div>
                    <div className="d-inline">
                        <input type="checkbox" onClick={handleClick} name="phase" id="completed" value="completed"/>
                        <label htmlFor="completed">Completed</label>
                    </div>
                </div>
            </form>
        </header>
    )
}

export default SearchBar;

在第二页上,我将单击“详细信息”按钮。代码:

import React from 'react';
import { Link } from 'react-router-dom';

import { FaAngleDoubleRight } from 'react-icons/fa';
import * as ROUTES from '../Constants/routes';

const ProgramCards = ({ program }) => {
    return (
        <div className="card">
            <div className="card-header">
                <h1 className="p-name">{program.name}</h1>
            </div>

            <div className="card-body">
                <h3 className="p-category">{program.category}</h3>
                <small className="p-phase">{(program.phase).replace('_', ' ')}</small>
                <p className="p-description">{program.shortDescription}</p>
            </div>

            <div>
                <div className="date-duration">
                    <small className="p-date">Start Date: {program.startDate}</small>
                    <small className="p-duration">Duration: {program.duration}</small>
                </div>
                <Link to={ROUTES.PROGRAM_DETAILS}>
                    <button className="card-button">
                        Details <FaAngleDoubleRight className="icon angled-icon" />
                    </button>
                </Link>
            </div>
        </div>
    )
}

export default ProgramCards;

我的问题是,为什么它不起作用?

在此之后,我尝试了另一种方法。它运行良好。

工作代码:

import React, { useEffect, useState } from 'react';
import { AiOutlineSearch } from "react-icons/ai";

const SearchBar = ({ totalPrograms, programs, setPrograms }) => {

    const [ scrolled, setScrolled ] = useState(false);

    const handleScroll = () => {
        if(window.scrollY > 10) {
            setScrolled(true);
        }
        else {
            setScrolled(false);
        }
    }

    useEffect(() => {
        window.addEventListener('scroll', handleScroll);

        return () => {window.removeEventListener('scroll', handleScroll)};
    },[]);

    const handleSubmit = (event) => {
        event.preventDefault();

        const searchProgramName = document.getElementById('search').value;
        
        if(searchProgramName) {
            setPrograms(
                programs.filter(program => 
                    program.name.toLowerCase().includes(searchProgramName)
                )
            );
        }
        else {
            handleClick();
        }
    }

    const handleClick = () => {
        const allPhase = document.getElementsByName('phase');
        const checkedPhaseValue = [];

        allPhase.forEach(phase => {
            if(phase.checked) {
                checkedPhaseValue.push(phase.value);
            }
        });

        setPrograms(checkedPhaseValue.length ?
            totalPrograms.filter(
                program => checkedPhaseValue.includes(program.phase.toLowerCase())
            )
            : totalPrograms
        );
    }

    return (
        <header className={`container header-sb ${scrolled && 'h-shadow'}`}>
            <form className="container container-center" onSubmit={handleSubmit}>
                <div className="type-search">
                    <AiOutlineSearch className="icon"/>
                    <input id="search" type="search" placeholder="search by program name"/>
                </div>
                <div className="checkbox-container">
                    <div className="d-inline">
                        <input type="checkbox" onClick={handleClick} name="phase" id="open_application" value="application_open"/>
                        <label htmlFor="open_application">Open Application</label>
                    </div>
                    <div className="d-inline">
                        <input type="checkbox" onClick={handleClick} name="phase" id="application_in_review" value="application_review"/>
                        <label htmlFor="application_in_review">Application in Review</label>
                    </div>
                    <div className="d-inline">
                        <input type="checkbox" onClick={handleClick} name="phase" id="in_progress" value="in_progress"/>
                        <label htmlFor="in_progress">in Progress</label>
                    </div>
                    <div className="d-inline">
                        <input type="checkbox" onClick={handleClick} name="phase" id="completed" value="completed"/>
                        <label htmlFor="completed">Completed</label>
                    </div>
                </div>
            </form>
        </header>
    )
}

export default SearchBar;

标签: htmlreactjs

解决方案


您的第二种方法是更好的反应方式。通常不鼓励自己查询由 react 管理的 DOM。如果您需要 DOM 节点,请使用 ref。

第一个示例不起作用,因为handleScroll每次组件重新渲染时都会重新创建。因此,删除监听器不会删除原始监听器,因为引用的函数handleScroll已更改。

因此,当您的组件卸载时,侦听器不会被正确删除,但 react 会删除 DOM 节点。下次滚动时仍会调用处理程序,但您尝试查询的节点不再存在。

您必须在其中创建侦听器,useEffect以便removeEventListener引用正确的函数:

useEffect(() => {
    const handleScroll = () => setScrolled(window.scrollY > 10);

    window.addEventListener('scroll', handleScroll);

    return () => window.removeEventListener('scroll', handleScroll);
},[]);

或者,您可以使用useRef创建对侦听器函数的稳定引用。


推荐阅读