DEV Community

Discussion on: Tracking Time with React Hooks

 
pke profile image
Philipp Kursawe • Edited

OK, I have the tests working now and they instantly revealed some bugs in the hook. Getting tests to run properly (for a react-native project if that matters) was the main challenge.
It involved a custom jest-preset to work around some promise related bug:

See solution and discussion

jest-preset.js

// Because fake timers did not work
// See:
// https://github.com/facebook/jest/issues/10221#issuecomment-730305710
// https://github.com/sbalay/without_await/commit/64a76486f31bdc41f5c240d28263285683755938
const reactNativePreset = require("react-native/jest-preset")

module.exports = Object.assign({}, reactNativePreset, {
  setupFiles: [require.resolve("./save-promise.js")]
    .concat(reactNativePreset.setupFiles)
    .concat([require.resolve("./restore-promise.js")]),
})
Enter fullscreen mode Exit fullscreen mode

This test now runs green:

import { act, renderHook, cleanup } from "@testing-library/react-hooks"
import { jest } from "@jest/globals"

import useDateTime from "./useDateTime"

describe("useDateTime hook", () => {
  beforeAll(() => {
    jest.useFakeTimers("modern")
  })

  afterEach(cleanup)

  afterAll(() => {
    jest.useRealTimers()
  })

  it("should report time updates every 30 minutes", () => {
    jest.setSystemTime(new Date("2020-11-19T10:00:00.000Z"))
    const { result } = renderHook(() => useDateTime("minute", 30))
    expect(result.current.toISOString()).toEqual("2020-11-19T10:00:00.000Z")
    act(() => jest.advanceTimersByTime(1000 * 30 * 60))
    expect(result.current.toISOString()).toEqual("2020-11-19T10:30:00.000Z")
  })
})
Enter fullscreen mode Exit fullscreen mode

A bug it revealed was that the hook reported the estimated future date before the timeout actually happened. In your component this was not a problem, you did not know you were one second ahead of system time ;) That happened because it called setDate outside the timeout callback.


export default function useDateTime(threshold: Threshold, amount = 1) {
  const [date, setDate] = useState(startOfThreshold(threshold))
  const timer = useRef<ReturnType<typeof setTimeout>>()

  useEffect(() => {
    if (threshold) {
      function delayedTimeChange() {
        const next = msUntilNext(threshold, amount)
        timer.current = setTimeout(() => {
+         setDate(startOfThreshold(threshold))
          delayedTimeChange()
        }, next)
-       setDate(startOfThreshold(threshold))
      }
      delayedTimeChange()
    }
    return () => timer.current && clearTimeout(timer.current)
  }, [threshold, amount])

  return date
}
Enter fullscreen mode Exit fullscreen mode

So now everything works! Thanks again for the initial work!

Thread Thread
 
dcwither profile image
Devin Witherspoon • Edited

I’ll take a look, but this change likely breaks its ability to immediately respond to threshold changes e.g. second to minute.

I also wouldn’t expect the setDate to change unless the time has actually passed by more than a second since it goes to the start of the threshold.

Again, I haven’t confirmed this yet, but that’s my intuition.

EDIT: I have confirmed the tests pass without your changes. The break may be caused by your added functionality

You can pull the tests down from useDateTime Code Sandbox with Tests in a new Create React App with "export to zip" to confirm (Code Sandbox doesn't seem to play well with @jest/globals, and export to Zip wasn't working for me either).

I've included a test that I believe will fail with your change, but you can confirm that for your implementation.