Giraphi
Giraphi

Reputation: 1651

Calculate initial value in custom hook

In my react app, I have a custom hook useDataObject() that computes an object based on the value of another hook, e.g. useScreenWidth(). My problem is, that I don't where to place the calculation logic, so the object is ready immediately on the first return:


function useScreenWidth() {
    const [width, setWidth] = useState(window.innerWidth);

    useEffect(() => {
        const handleResize = () => setWidth(window.innerWidth);
        window.addEventListener("resize", handleResize);
        return () => window.removeEventListener("resize", handleResize);
    }, []);

    return width;
}

function useDataObject() {
    const width = useScreenWidth();
    const [dataObject, setDataObject] = useState();

    useEffect(() => {
        // Some logic that creates dataObject from width
        const dataObject = {
            firstValue: 2 * width,
            secondValue: width + 10
        };
        setDataObject(dataObject);
    }, [width]);

    return dataObject;
}

export default function App() {
    const dataObject = useDataObject();

    // Can I get rid of this check?
    // Currently needed to prevent "Can not read property firstValue of undefined
    if (!dataObject) {
        return null;
    }

    return (
        <>
            <div>Data object first value: {dataObject.firstValue}</div>
            <div>Data object second value: {dataObject.secondValue}</div>
        </>
    );
}

See this Sandbox

Because useDataObject does not have an initial state I need to wait for the first useEffect to finish until I get a valid dataObject. This means I need to always include a null check after calls to useDataObject().

What would be the best approach to execute the calculation logic in useDataObject() before the first return and then every time the width changes?

One approach I tried was creating an internal function that does the calculation:

function useDataObject() {
  const width = useScreenWidth();
  const [dataObject, setDataObject] = useState(calculateDataObject());

  function calculateDataObject() {
    // Some logic that creates dataObject from width
    const dataObject = {
      firstValue: 2 * width,
      secondValue: width + 10
    };
    return dataObject;
  }

  useEffect(() => {
    setDataObject(calculateDataObject());
  }, [width, calculateDataObject]);

  return dataObject;
}

But this looks pretty clunky and confusing. It also gives me a linting error telling me I should use useCallback(). Isn't there a more straight forward way? Thank you!

Upvotes: 2

Views: 858

Answers (1)

Giraphi
Giraphi

Reputation: 1651

Answering my own question. I found using the useMemo() hook (see docs) instead of useState() + useEffect() is a possible solution for my scenario:

function useDataObject() {
  const width = useScreenWidth();

  return useMemo(() => {
    // Do some calculation with width
    const dataObject = {
      firstValue: 2 * width,
      secondValue: width + 10
    };
    return dataObject;
  }, [width]);
}

Full example on codesandbox

  • The hook returns the computed dataObject immediately.
  • Every time [width] changes the calculation is redone and the new value is returned.
  • No code is duplicated or executed too often.

I can not guarantee that there is no better solution, but for my purpose it does exactly what I need. (Of course one could also integrate the useScreenWidth hook into the useDataObject Hook. But in my project, I need the useScreenWidth separately.)

Upvotes: 1

Related Questions