4

I am working on a form that has, among other fields, calendar entries - dates and times. The code is below:

  {dates.map((d, i) => (
      <div className="date-time-entry" key={i}>
        <div className="date-picker">
          <DatePicker
            selected={new Date(d.startDate)}
            dateFormat="dd MMM yyyy"
            onChange={date => {
              const newDates = [...dates]
              newDates[i].startDate = date
              setDates(newDates)
            }}
          />
          to
          <DatePicker
            selected={new Date(d.endDate)}
            dateFormat="dd MMM yyyy"
            onChange={date => {
              const newDates = [...dates]
              newDates[i].endDate = date
              setDates(newDates)
            }}
          />
          {i ? (
            <span
              onClick={() => {
                const newDates = dates.filter((d, k) => k !== i)
                setDates(newDates)
              }}
            >
              <FontAwesomeIcon icon={faTimes} />
            </span>
          ) : null}
        </div>
        {dates[i].times.map((t, j) => {
          return (
            <div className="time-picker" key={j}>
              at
              <input
                ref={(input) => { nameInput = input }}
                value={t.start}
                onChange={e => {
                  const newDates = [...dates]
                  newDates[i].times[j].start = e.target.value
                  setDates(newDates)
                }}
              />{' '}
              to{' '}
              <input
                ref={(input) => { nameInput = input }}
                value={t.end}
                onChange={e => {
                  const newDates = [...dates]
                  newDates[i].times[j].end = e.target.value
                  setDates(newDates)
                }}
              />
              <span
                onClick={() => {
                  const newTimes = dates[i].times.filter((t, k) => k !== j)
                  const newDates = [...dates]
                  newDates[i].times = newTimes
                  setDates(newDates)
                }}
              >
                <FontAwesomeIcon icon={faTimes} />
              </span>
            </div>
          )
        })}
        <div
          className="add-time"
          onClick={() => {
            const newDates = [...dates]
            newDates[i].times = [
              ...newDates[i].times,
              { start: '12:00', end: '13:00' }
            ]
            setDates(newDates)
          }}
        >
          Add Time
        </div>
      </div>
    )
  )}

Then there is the useState() hook defined as:

const [dates, setDates] = useState([
  {
    startDate: new Date(),
    endDate: new Date(),
    times: [{ start: '12:00', end: '13:00' }]
  }
])

The problem that I am having is that whenever onChange is called - each keystroke the rerender occurs and my component loses focus.

I tried using onBlur and defaultValue instead of onChange, but there is also a focus problem - two clicks are needed for the focus to switch because the first one is consumed by rerendering.

I also tried adding timeout to deal with onBlur "losing" one click as below:

<input
  defaultValue={t.start}
  onBlur={e => {
    const value = e.target.value
    timeout = setTimeout(() => {
      const newDates = [...dates]
      newDates[i].times[j].start = value
      setDates(newDates)
    }, 0)
  }}
  onFocus={() => {
    clearTimeout(timeout)
  }}
/>

The last version does not lose focus anymore, yet instead, it loses state updates. Any suggestions as to how better handle this, or maybe even fix my existing code?

Igor Shmukler
  • 1,742
  • 3
  • 15
  • 48
  • This is just a hint, but I'm pretty sure the problem is coming from a changing key. When the key changes, the component, instead of being updated on the same instance, is unmounted and a new instance is mounted. This also involves DOM objects, which will clear focus. So this may not be the answer, but it's a common cause. – John Weisz Mar 09 '20 at 11:09

1 Answers1

0

I think it is not good idea to have the following code:

{dates[i].times.map((t, j) => {
      return (
        <div className="time-picker" key{j}>
          at
          <input
            ref={(input) => { nameInput = input }}
            value={t.start}
            onChange={e => {
              const newDates = [...dates]
              newDates[i].times[j].start = e.target.value
              setDates(newDates)
            }}
          />{' '}
          to{' '}
          <input
            ref={(input) => { nameInput = input }}
            value={t.end}
            onChange={e => {
              const newDates = [...dates]
              newDates[i].times[j].end = e.target.value
              setDates(newDates)
            }}
          />
          <span
            onClick={() => {
              const newTimes = dates[i].times.filter((t, k) => k !== j)
              const newDates = [...dates]
              newDates[i].times = newTimes
              setDates(newDates)
            }}
          >
            <FontAwesomeIcon icon={faTimes} />
          </span>
        </div>
      )
    })}

Each time, any change in dates, it will re-render all components again. You should have every time picker as a separate component like the following:

const changeHandler = (i, j, x, val) => {
  const newDates = [...dates]
  newDates[i].times[j][x] = val
  setDates(newDates)
}
const clickHandler = (i, j) => {
  const newTimes = dates[i].times.filter((t, k) => k !== j)
  const newDates = [...dates]
  newDates[i].times = newTimes
  setDates(newDates)
}
{dates[i].times.map((t, j) => {
  return (
    <MyTimePicker time={t} k={"time_" + j} changeHandler={( val,  x="start" ) => changeHandler(i, j, x, val)} clickHandler={() => clickHandler(i, j)}>
  )
 })}

and in another file "my-timepicker.js", you should define your component:

import React, { useRef } from "react";
const areEqual = (prevProps, nextProps) => {
  if(JSON.stringify(prevProps.time) == JSON.stringify(nextProps.time)) return true;
  return false;
}
const MyTimePicker = React.memo(({changeHandler, clickHandler, time }) => {
   const atInputRef = useRef();
   const toInputRef = useRef();
   return ( 
     <div className="time-picker">
       at
       <input
          ref={atRef}
          value={time.start}
          onChange={e => {
            changeHandler(e.target.value)
          }}
       />{' '}
       to{' '}
       <input
         ref={toRef}
         value={time.end}
         onChange={e => {
           changeHandler(e.target.value, "end")
         }}
       />
       <span
         onClick={clickHandler}
       >
         <FontAwesomeIcon icon={faTimes} />
       </span>
     </div>
   )
}, areEqual);
Mohamed Magdy
  • 535
  • 5
  • 9
  • Tried your code. Think that your `nameInput` is maybe undefined. Don't see any difference in the behaviour. – Igor Shmukler Mar 09 '20 at 10:39
  • @IgorShmukler I have edited the answer to fix nameInput and also I used React.memo to make sure that the component doesn't rerender if time value doesn't change – Mohamed Magdy Mar 09 '20 at 11:09
  • Now, it works exactly like the original version - loses focus. – Igor Shmukler Mar 09 '20 at 11:27
  • @IgorShmukler Also, I noticed a mistake in the code. Your `keys` should have a prefix. I added `time` prefix for the time array and at the top of your code, you should change the dates to the following: `{dates.map((d, i) => (
    `
    – Mohamed Magdy Mar 09 '20 at 11:28
  • I don't think that this is the problem. Did you try running your own code? Thank you. – Igor Shmukler Mar 09 '20 at 11:30