DCodeMania
DCodeMania

Reputation: 1157

Getting always initial value of state after updating the state from the local storage

I'm using react-timer-hook package in my next.js project to display a timer like you can see in the screenshot below:

Timer Screenshot

Now the issue is I want to persist this timer elapsed time into the local storage and whenever the page reloads manually then I need the Timer to start from that specific elapsed time that I'm trying to get from the local storage, but whenever I reload the page manually then Timer starts from the initial state's value. below are the codes:

Timer Component

function Timer({ seconds, minutes, hours }) {
    return (
        <Typography variant="h5" fontWeight={'bold'} component={'div'}>
            <span>{String(hours).padStart(2, '0')}</span>:
            <span>{String(minutes).padStart(2, '0')}</span>:
            <span>{String(seconds).padStart(2, '0')}</span>
        </Typography>
    );
}

I'm adding 3600 seconds into expiryTimestamp i.e., current date and time to get one hour of Timer.

let expiryTimestamp = new Date();
expiryTimestamp.setSeconds(expiryTimestamp.getSeconds() + 3600);

Aslo I'm using another state with same 3600 seconds initial value

const [elapsed, setElapsed] = useState(3600);

I'm using useEffect and decrementing the elapsed value on every second into local storage.

useEffect(() => {
    const interval = setInterval(() => {
        localStorage.setItem('elapsed', JSON.stringify(elapsed--));
    }, 1000);
    return () => clearInterval(interval);
}, [elapsed]);

Now I'm getting the elapsed value from the local storage

useEffect(() => {
    const elapsed = localStorage.getItem('elapsed');
    if (elapsed) {
        setElapsed(JSON.parse(elapsed));
    }
}, []);

Again I'm using another variable to create current date and time + elapsed value

let currentTime = new Date();
currentTime.setSeconds(currentTime.getSeconds() + elapsed);

Finally I'm passing the currentTime in useTimer hook

const { seconds, minutes, hours } = useTimer({
    expiryTimestamp: currentTime,
    onExpire: handleForm,
});

Elapsed time is properly storing in the local storage, but still Timer starts from 3600 seconds.

Upvotes: 1

Views: 510

Answers (2)

Moorthy G
Moorthy G

Reputation: 1471

We can use expiryTimestamp value of use timer as function to initiate its value. Check the following component

import { useEffect } from 'react';
import { useTimer } from 'react-timer-hook';

export default function Timer() {
  const { seconds, minutes, hours } = useTimer({
    expiryTimestamp: () => {
      /** determine the expiry time stamp value all at once */
      const time = new Date(),
        elapsedTime = Number(window.localStorage.getItem('elapsed')) || 3600;
      time.setSeconds(time.getSeconds() + elapsedTime);
      return time;
    },
    onExpire: () => alert('expired')
  });

  /** update elapsed value in local storage */
  useEffect(() => {
    const elapsedSeconds = hours * 60 * 60 + minutes * 60 + seconds;
    window.localStorage.setItem('elapsed', elapsedSeconds);
  }, [seconds]);

  return (
    <div>
      <span>{String(hours).padStart(2, '0')}</span>:
      <span>{String(minutes).padStart(2, '0')}</span>:
      <span>{String(seconds).padStart(2, '0')}</span>
    </div>
  );
}

If you're using this component in next.js. You should use dynamic import with ssr disabled. otherwise you'll receive an error because SSR doesn't recognize the window.localStorage api. Check below


import dynamic from 'next/dynamic';
import React from 'react';

const Timer = dynamic(() => import('./timer'), { ssr: false });

export default function Index = () => {
  return <Timer />;
};


Upvotes: 4

Tom
Tom

Reputation: 1188

It looks like your first useEffect hook is overwriting the value in local storage before you set your state from what's stored there.

Hooks fire in the order in which they are defined within a component, so your component is doing:

  1. Set state to 3600
  2. UseEffect where we set the local storage
  3. UseEffect where we read local storage (whose value has just been overwritten to the default 3600) and set the state to that

You could try checking the local storage value and starting your interval in the same hook.

I'd first start by starting my default state with undefined

const [elapsed, setElapsed] = useState();

This lets me control the starting of the timer entirely within the useEffect

useEffect(() => {
    // make sure your first action is to get the elapsed time in local storage
    const storedElapsed = localStorage.getItem('elapsed');
    // work out if this is the initial load or not
    if (!elapsed) {
        // component state is not yet set, so use either local storage or the default
        const newElapsedState = JSON.parse(storedElapsed) || 3600;
        setElapsed(newElapsedState);
    } else {
        // we now know that state is set correctly, so can ignore local storage and start the timer
        const interval = setInterval(() => {
            localStorage.setItem('elapsed', JSON.stringify(elapsed--));
        }, 1000);
        return () => clearInterval(interval);
    }
}, [elapsed]);

You will need to handle your first render being undefined, but after that your flow should be consistent as you have control over what is being set in what order.

You can control this by the order in which you define your useEffects, but I personally find this way a bit clearer as the logic is grouped together.

Upvotes: 0

Related Questions