Search code examples
reactjsreact-hooksuse-effect

useEffect inside customHook not happens sometimes (from unknown reason). Using useRef


I have this custom hook which supposed to make debounce email validation. Suddenly I notice that it's not happening in all types.

Sometimes in the first types it's happen sometimes not.

See the log of "useEffect" which won't happen (for me - in each case) with any type. And when it's happening it's taking the previous value.

the Custom hook:

export function useDebounceEmailValidation(value, delay) {
console.log("useDebounceEmailValidation ? ")
// State and setters for debounced value
const [valid, setValid] = useState(true);
const initRun = useRef(true);
console.log("init run = " , initRun.current)


useEffect(
    () => {
        console.log("useEffect?"); //---------------------------> this not happening on each render 
        //we don't want to do it on initial running
        if(initRun.current){
            initRun.current = false;
        }
        else{
            // Update debounced value after delay
            const handler = setTimeout(() => {
                console.log("validating mail - " ,value);
                setValid(validateEmail(value));
                // setDebouncedValue(value);
            }, delay);
            // Cancel the timeout if value changes (also on delay change or unmount)
            // This is how we prevent debounced value from updating if value is changed ...
            // .. within the delay period. Timeout gets cleared and restarted.
            return () => {
                clearTimeout(handler);
            };
        }

    },
    [value, delay] // Only re-call effect if value or delay changes
);
return valid;

}

the form component:

import React, {useLayoutEffect, useRef, useState} from 'react';
import Button from 'react-bootstrap/Button';
import {useDebounceEmailValidation} from "./utils-hooks";
import {Alert, Col, Form} from "react-bootstrap";

export function SubscribersForm() {
const [details, setDetails] = useState({
    firstName: "",
    lastName: "",
    email: "",
    tel: ""
});

const [popupMsg, setPopupMsg] = useState("default");
const [showMsg, setShowMsg] = useState(false);
const [isError, setIsError] = useState(false);
const emailChange = useRef(false);
const [validated, setValidated] = useState(false);

//For cases we need to access the email through local state and not by state
// (the value will persist between component re-rendering and the reference updating won't trigger a component re-rendering)
const emailRef = useRef("");
const validEmail = useDebounceEmailValidation(emailRef.current, 600);

// // general layout effect - will be happen on each component update - for DEBUG
// useLayoutEffect(() => {
//     console.log("details = ", details);
// });

//happening after change in the popup message
useLayoutEffect(() => {
    setTimeout(() => {
        //resetting the msg
        setPopupMsg("");
        setShowMsg(
            false);

    }, 2000);
}, [popupMsg])

//happen after change in the details
useLayoutEffect(() => {
    //handling email changing (validation)
    if (emailChange.current) {
        emailRef.current = details.email;
        console.log("email.current = " , emailRef.current)
    }
}, [details]);

const handleChange = (ev) => {
    ev.persist();
    if (ev.target.name === "email" ) {
        emailChange.current = true;

    } else {
        emailChange.current = false;
    }
    setDetails(prevDetails => ({
        ...prevDetails,
        [ev.target.name]: ev.target.value
    }));

}

const onSubmit = (ev) => {

    const form = ev.currentTarget;

    //The default validation for the form
    if (form.checkValidity() === false || !validEmail) {
        ev.preventDefault();
        ev.stopPropagation();
        setValidated(true);
        return;
    }

    ev.preventDefault();
    alert("Those are the details - you can send it from here to the server !!! :)  \n\n" +
        "name = " + details.firstName + " " + details.lastName
        + "\nemail = " + details.email
        + "\nphone = " + details.tel);

    //we set validation to false, because by default you don't want to show validation
    setValidated(false);

    setPopupMsg("Your details have been successfully saved");
    setIsError(false);
    setDetails({
        firstName: "",
        lastName: "",
        email: "",
        tel: ""
    });

    setShowMsg(true);

}

return (
    <div className="subscribers-input">
        <h3>Subscribers - Form</h3>
        <Form className="needs-validation" noValidate validated={validated}
              onSubmit={onSubmit}>{/*start of the form block */}
            <Form.Row className="">{/*start of the form row of 12/12 columns*/}

                <Col xs={12} className="">
                    <Form.Group controlId="firstName" className="">
                        <Form.Control
                            type="text"
                            placeholder="First name"
                            value={details.firstName}
                            onChange={handleChange}
                            name="firstName"
                            required
                            size="sm"
                            aria-label="first name"

                        />


                    </Form.Group>

                </Col>
            </Form.Row>
            <Form.Row className="">

                <Col xs={12} className="">
                    <Form.Group controlId="lastName" className="">
                        <Form.Control
                            type="text"
                            placeholder="Last name"
                            value={details.lastName}
                            onChange={handleChange}
                            name="lastName"
                            required
                            size="sm"
                            aria-label="last name"

                        />


                    </Form.Group>

                </Col>
            </Form.Row>
            <Form.Row className="">

                <Col xs={12} className="">
                    <Form.Group controlId="email" className="">
                        <Form.Control
                            type="email"
                            placeholder="Email"
                            value={details.email}
                            onChange={handleChange}
                            name="email"
                            required
                            size="sm"
                            aria-label="email"
                            isInvalid={!validEmail}

                        />
                        <Form.Control.Feedback type="invalid">Email is Invalid</Form.Control.Feedback>


                    </Form.Group>

                </Col>
            </Form.Row>
            <Form.Row className="">

                <Col xs={12} className="">
                    <Form.Group controlId="tel" className="">
                        <Form.Control
                            type="tel"
                            placeholder="Phone"
                            value={details.tel}
                            onChange={handleChange}
                            name="tel"
                            required
                            size="sm"
                            aria-label="phone"

                        />


                    </Form.Group>

                </Col>
            </Form.Row>
            {showMsg &&
            <Alert variant={isError ? 'danger' : 'success'}>{popupMsg}</Alert>
            }
            <Button type="submit" size="sm">Save</Button>

        </Form>
    </div>
)

}

See here how the log not always happening.


Solution

  • there are two things to know: first, custom hooks only rerun when the component that we use our custom hook in (in our case, "SubscribersForm"), rerenders. second, the useEffect dependency array checks the equality of objects by references.

    so to be sure that useEffect can intercept changes in its dependency array object we should pass new references for that object.

    in the main component "SubscribersForm" you pass a reference to your "useDebounceEmailValidation" custom hook, so when you change the value of emailRef.current, no rerenders happen, and also the reference of the object is the same as before. you should use states in these cases