skip to Main Content

I’m trying to learn React by solving a basic JavaScript problem in React. I googled but still no luck, not sure how to solve it. Here is a repro on stackblitz.

import { FC } from 'react';

import './style.css';

export const App: FC<{ name: string }> = ({ name }) => {
  const emailInput = document.querySelector('#email');
  const emailRegex = '//';
  emailInput.addEventListener('keyup', (e) => {
    console.log((e.target as HTMLButtonElement).value);
    if (emailRegex.test((e.target as HTMLButtonElement).value)) {
      emailInput.parentElement.classList.add('valid');
    } else {
      emailInput.parentElement.classList.remove('valid');
    }
  });

  return (
    <div>
      <form>
        <div className="field valid">
          <label for="email">Enter your email:</label>
          <input type="text" name="email" id="email" />
          <span className="material-icons tick">done</span>
        </div>
      </form>
    </div>
  );
};

I’m getting this error:

Error in /~/src/App.tsx (9:16)
Cannot read properties of null (reading 'addEventListener')

2

Answers


  1. You should avoid relying on the DOM with React. Instead, use states to store the value of the input and apply the class dynamically:

    import { FC, useState } from 'react';
    
    import './style.css';
    
    export const App: FC<{ name: string }> = ({ name }) => {
        const emailRegex = /blah/;
    
        const [value, setValue] = useState('');
        const isValid = emailRegex.test(value);
    
        return (
            <div>
                <form>
                    <div className={`field ${isValid ? 'valid' : ''}`}>
                        <label htmlFor="email">Enter your email:</label>
                        <input
                            type="text"
                            name="email"
                            id="email"
                            value={value}
                            onChange={(e) => setValue(e.target.value)}
                        />
                        <span className="material-icons tick">done</span>
                    </div>
                </form>
            </div>
        );
    };
    

    Demo

    Login or Signup to reply.
  2. The problem is that the component tree hasn’t rendered by the time your above code is running. You could fix this by wrapping your event listener code in a useEffect:

    import { FC, useEffect } from 'react';
    
    import './style.css';
    
    export const App: FC<{ name: string }> = ({ name }) => {
      useEffect(() => {
        const emailInput = document.querySelector('#email');
        const emailRegex = /todo/;
        const handleKeyup = (e) => {
          console.log((e.target as HTMLButtonElement).value);
          if (emailRegex.test((e.target as HTMLButtonElement).value)) {
            emailInput.parentElement.classList.add('valid');
          } else {
            emailInput.parentElement.classList.remove('valid');
          }
        }
        emailInput.addEventListener('keyup', handleKeyup);
    
        return () => emailInput.removeEventlistener('keyup', handleKeyup)
      }, []);
    
    
      return (
        <div>
          <form>
            <div className="field valid">
              <label for="email">Enter your email:</label>
              <input type="text" name="email" id="email" />
              <span className="material-icons tick">done</span>
            </div>
          </form>
        </div>
      );
    };
    

    this would make the code wait to execute until after react has rendered the DOM. This is unidiomatic however. React really encourages you to write code in a declarative style. Try to avoid manipulating the DOM and instead express UI as a function of state if at all possible. We would begin by adding the event listener in the jsx:

    import { FC } from 'react';
    
    import './style.css';
    
    export const App: FC<{ name: string }> = ({ name }) => {
      const emailRegex = /todo/;
    
      const handlekeyup = (e) => {
        console.log((e.target as HTMLButtonElement).value);
        if (emailRegex.test((e.target as HTMLButtonElement).value)) {
          emailInput.parentElement.classList.add('valid');
        } else {
          emailInput.parentElement.classList.remove('valid');
        }
      }
    
      return (
        <div>
          <form>
            <div className="field valid">
              <label for="email">Enter your email:</label>
              <input type="text" name="email" id="email" onKeyup={handleKeyup} />
              <span className="material-icons tick">done</span>
            </div>
          </form>
        </div>
      );
    };
    

    this removes the imperative event listener manipulation but we still have the classes being manipulated. You could go about fixing this by creating some state to hold whether the input is valid or not:

    import { FC, useState } from 'react';
    
    import './style.css';
    
    export const App: FC<{ name: string }> = ({ name }) => {
      const [emailValid, setEmailValid] = useState(false);
    
      const emailRegex = /todo/;
      const handlekeyup = (e) => {
        console.log((e.target as HTMLButtonElement).value);
        setEmailValid(
            emailRegex.test((e.target as HTMLButtonElement).value)
        );
      }
    
      return (
        <div>
          <form>
            <div className={`field ${emailValid ? 'valid' : ''}`}>
              <label for="email">Enter your email:</label>
              <input type="text" name="email" id="email" onKeyup={handleKeyup} />
              <span className="material-icons tick">done</span>
            </div>
          </form>
        </div>
      );
    };
    

    doing things this way is alright, but by overwhelming convention we almost always store the current value of the input as a state and then derive information based on that. This will help minimize your state footprint and help to make invalid states unrepresentable as your application grows in complexity. We can also inline our event handler if desired since it’s getting pretty small.

    import { FC, useState } from 'react';
    
    import './style.css';
    
    export const App: FC<{ name: string }> = ({ name }) => {
      const [email, setEmail] = useState('');
    
      const emailRegex = /todo/;
      const emailValid = emailRegex.test((e.target as HTMLButtonElement).value);
    
      return (
        <div>
          <form>
            <div className={`field ${emailValid ? 'valid' : ''}`}>
              <label for="email">Enter your email:</label>
              <input
                type="text"
                name="email"
                id="email"
                onKeyup={(e) => setEmail(e.target.value)}
              />
              <span className="material-icons tick">done</span>
            </div>
          </form>
        </div>
      );
    };
    

    the last thing is that by convention in react we use the onChange event as a sensible default.

    import { FC, useState } from 'react';
    
    import './style.css';
    
    export const App: FC<{ name: string }> = ({ name }) => {
      const [email, setEmail] = useState('');
    
      const emailRegex = /todo/;
      const emailValid = emailRegex.test((e.target as HTMLButtonElement).value);
    
      return (
        <div>
          <form>
            <div className={`field ${emailValid ? 'valid' : ''}`}>
              <label for="email">Enter your email:</label>
              <input
                type="text"
                name="email"
                id="email"
                onChange={(e) => setEmail(e.target.value)}
              />
              <span className="material-icons tick">done</span>
            </div>
          </form>
        </div>
      );
    };
    

    we now have idiomatic react.

    Hopefully this long winded explanation helps you understand how to think about going from imperative DOM manipulation to declarative idiomatic react

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search