React Typescript - Math.random gives wrong result even tho min value and max value is set

Viewed 131

Main TSX File

  const [minNumber, setMinNumber] = useLocalStorage('minRange', 1);
  const [maxNumber, setMaxNumber] = useLocalStorage('maxRange', 10);

...

  const getRandomNumber = () => {
    //Return a random number between minNumber and maxNumber:
    console.log("min: " + minNumber + " max: " + maxNumber)
    let randomNr = Math.floor(Math.random() * (maxNumber - minNumber + 1) + minNumber);
    setRandomNumber(randomNr);
  }

Local Storage hook:

import { useState } from 'react';

export function useLocalStorage(key, defaultValue) {
    const getInitialValue = () => localStorage.getItem(key) ?? defaultValue;
    const [value, setValue] = useState(getInitialValue);
    const setAndStoreValue = (newValue) => {
        if (newValue !== '') {
            setValue(newValue);
            localStorage.setItem(key, newValue)
        }
    }
    return [value, setAndStoreValue];
};

To demonstrate the problem. I have created a codesandbox link: https://codesandbox.io/s/hdwqy?file=/src/App.tsx

How to recreate the problem:

  • By default minValue is 1 and maxValue is 10
  • set the minValue to 8 and maxValue 9 (click "Change min & max value" button in codesandbox)
  • run the getRandomNumber() function. Outputs: 8 or 9 - works! (click "New random Number" button in codesandbox)
  • refresh the page and you will get the data stored in local storage min = 8, and max = 9
  • run the getRandomNumber() function. outputs 0 or 1 - wrong outputs! (click "New random Number" button in codesandbox)
  • The problem is since the difference between min and max is 2, it gives 0 and 1. If min was 8 and max was 10 then it will output 0, 1, or 2.

I can't seem to figure out why this is happening, since it works fine if I set the min and max again and rerun the random function. I would also like to mention that in both cases the console.log outputs min: 8 max: 9, Therefore it seems local storage is loading the value correctly. It is quite odd.

1 Answers

The problem can be reduced to this:

const minNumber = '8';
const maxNumber = '9';
const getRandomNumber = () => {
  console.log("min: " + minNumber + " max: " + maxNumber);
  console.log('part', maxNumber - minNumber + 1);
  const result = Math.random() * 2 + '8';
  console.log(result);
};

getRandomNumber();

Local Storage values are always strings. This:

let randomNr = Math.floor(
  Math.random() * (maxNumber - minNumber + 1) + minNumber
);

turns into, with 8 and 9:

let randomNr = Math.floor(
  Math.random() * 2 + '8'
);

turning into something like 0.234458 or 1.63138 - the + concatenates a trailing 8 onto the decimal portion, and then the Math.floor shaves it off, so you're still left with a number that's either 0 or 1.

Cast the storage values to numbers first, perhaps by having an mapper function for useLocalStorage:

export function useLocalStorage(key, defaultValue, mapper = str => str) {
    const getInitialValue = () => localStorage.getItem(key) !== null
      ? mapper(localStorage.getItem(key))
      : defaultValue;
    const [value, setValue] = useState(getInitialValue);
    const setAndStoreValue = (newValue) => {
        if (newValue !== '') {
            setValue(newValue);
            localStorage.setItem(key, newValue)
        }
    }
    return [value, setAndStoreValue];
};
const [minNumber, setMinNumber] = useLocalStorage('minRange', 1, Number);
const [maxNumber, setMaxNumber] = useLocalStorage('maxRange', 10, Number);

This problem could have been avoided by utilizing TypeScript more, and type your variables properly (avoid any - that defeats the point of TypeScript) - the type of minNumber and maxNumber being a string or number instead of just a number would have tipped you off.

Use generics too:

function useLocalStorage<T>(
  key: string,
  defaultValue: T,
  mapperToValue: (lsValue: string) => T,
  mapperToLS: (value: T) => string
) {
  const getInitialValue = () => {
    const lsValue = localStorage.getItem(key);
    return lsValue !== null ? mapperToValue(lsValue) : defaultValue;
  };
  const [value, setValue] = useState(getInitialValue);
  const setAndStoreValue = (newValue: T) => {
    setValue(newValue);
    localStorage.setItem(key, mapperToLS(newValue));
  };
  return [value, setAndStoreValue] as const;
}

export default function App() {
  const [minNumber, setMinNumber] = useLocalStorage("minRange", 1, Number, String);
  const [maxNumber, setMaxNumber] = useLocalStorage("maxRange", 10, Number, String);
  const [rndNumber, setRndNumber] = useState(0);
  // ...
Related