Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cannot set useState hook value to opposite boolean

I'm creating a DatePicker and using useState hook to manage it's visibility. On div click I've added the event listener which changes value, but it didn't work as I expected. It works only the first time, so initial value changes to true, but on second and third clicks this value stays to true and DatePicker stays visible on click.

This is DatePicker

import React, { useState } from 'react';
import OutsideClickHandler from 'react-outside-click-handler';
import { renderInfo, getWeeksForMonth } from './utils';
import {
    renderMonthAndYear,
    handleBack,
    handleNext,
    weekdays,
} from '../../utils';

const DatePicker = ({
    isOpen,
    setIsOpen,
    selected,
    setSelected,
    dayClick,
    dayClass,
}) => {
    const startDay = new Date().setHours(0, 0, 0, 0);

    const [current, setCurrent] = useState(new Date(startDay));

    const weeks = getWeeksForMonth(current.getMonth(), current.getFullYear());

    function handleClick(date) {
        if (date > startDay) {
            setSelected(date);
            setCurrent(date);
            setIsOpen(false);

            if (dayClick) {
                dayClick(date);
            }
        }
    }

    return (
        <div className="DatePicker-container">
            <div
                tabIndex="0"
                role="button"
                className="DatePicker-info"
                onKeyPress={e => {
                    if (e.which === 13) {
                        setIsOpen(!isOpen);
                    }
                }}
                onClick={e => {
                    setIsOpen(!isOpen);
                }}
            >
                {renderInfo(selected)}
            </div>
            {isOpen && (
                <OutsideClickHandler onOutsideClick={() => setIsOpen(false)}>
                    <div className="DatePicker">
                        <div className="DatePicker__header">
                            <span
                                role="button"
                                onClick={() => handleBack(current, setCurrent)}
                                className="triangle triangle--left"
                            />

                            <span className="DatePicker__title">
                                {renderMonthAndYear(current)}
                            </span>

                            <span
                                role="button"
                                onClick={() => handleNext(current, setCurrent)}
                                className="triangle triangle--right"
                            />
                        </div>

                        <div className="DatePicker__weekdays">
                            {weekdays.map(weekday => (
                                <div
                                    key={weekday}
                                    className="DatePicker__weekday"
                                >
                                    {weekday}
                                </div>
                            ))}
                        </div>
                        {weeks.map((week, index) => (
                            <div
                                role="row"
                                key={index}
                                className="DatePicker__week"
                            >
                                {week.map((date, index) =>
                                    date ? (
                                        <div
                                            role="cell"
                                            key={index}
                                            onClick={() => handleClick(date)}
                                            className={dayClass(date)}
                                        >
                                            {date.getDate()}
                                        </div>
                                    ) : (
                                        <div
                                            key={index}
                                            className="DatePicker__day--empty"
                                        />
                                    ),
                                )}
                            </div>
                        ))}
                    </div>
                </OutsideClickHandler>
            )}
        </div>
    );
};

export default DatePicker;

DateRangePicker which uses two.

import React, { useState } from 'react';
import DatePicker from './DatePicker';
import DatePickerContext from './DatePickerContext';
import './DatePicker.scss';

const DateRangePicker = () => {
    const startDay = new Date().setHours(0, 0, 0, 0);

    const [isOpen, setIsOpen] = useState(false);
    const [isSecondOpen, setIsSecondOpen] = useState(false);
    const [selected, setSelected] = useState(new Date(startDay));
    const [secondSelected, setSecondSelected] = useState(new Date(startDay));

    function dayClass(date) {
        if (
            selected.getTime() === date.getTime() ||
            (date >= selected && date <= secondSelected)
        ) {
            return 'DatePicker__day DatePicker__day--selected';
        }
        if (date < startDay || date < selected) {
            return 'DatePicker__day DatePicker__day--disabled';
        }
        return 'DatePicker__day';
    }

    function dayClick(date) {
        setSecondSelected(date);
        setIsSecondOpen(true);
    }

    return (
        <DatePickerContext.Provider>
            <div className="DatePicker-wrapper">
                <DatePicker
                    key={1}
                    isOpen={isOpen}
                    setIsOpen={setIsOpen}
                    selected={selected}
                    setSelected={setSelected}
                    dayClick={dayClick}
                    dayClass={dayClass}
                />
                <DatePicker
                    key={2}
                    isOpen={isSecondOpen}
                    setIsOpen={setIsSecondOpen}
                    selected={secondSelected}
                    setSelected={setSecondSelected}
                    dayClass={dayClass}
                />
            </div>
        </DatePickerContext.Provider>
    );
};

export default DateRangePicker;
like image 815
Mirian Okradze Avatar asked Jul 27 '19 11:07

Mirian Okradze


1 Answers

a) It is a good practice to use functional updates to make sure to use correct "current" value when the next state is dependent on the previous (== current) state:

setIsOpen(currentIsOpen => !currentIsOpen)

b) It's very hard to reason about the next state when it gets updated by multiple handlers executed for the same event. Following 2 handlers might execute on the same click (the 1st div is "outside"):

<div ... onClick={e => setIsOpen(!isOpen)}>
<OutsideClickHandler onOutsideClick={() => setIsOpen(false)}>

If onOutsideClick executes first, then React re-renders with isOpen=false, and then onClick executes second, it would set isOpen=true as you observe - I don't see how the re-render could happen between, but maybe OutsideClickHandler is doing something nefarious or your code is more complicated than in the question ¯\_(ツ)_/¯

To enforce only 1 event handler:

<OutsideClickHandler onOutsideClick={(e) => {
  e.stopPropagation();
  setIsOpen(false);
}}>
like image 98
Aprillion Avatar answered Nov 12 '22 03:11

Aprillion