1
votes

Hey guys I am new to using React Hooks but couldn't find it on google directly. I am attempting to nest setState callback so that the state can update synchronously but haven't had success as I get undefined values. The values within the state are reliant on other values within my state so I would ideally like it to run synchronously. I tried nesting it below, which works when it is a class component and use this.setState and use the callback, but doesn't work when I attempt to use react hooks within a functional class.

Here is my code:

const [state, setState] = useState({numCols: 0, numRows: 0, cardWidth: 0, cardHeight: 0, containerWidth: 0});
  
  const {
    sheetList,
    sheetsTotalCount,
    sheetsMetadata,
    id,
    projectId,
    onEditButtonClick,
    handleInfiniteLoad,
    sheetsAreLoading,
    classes,
    permissions,
    onItemClick,
  } = props;

  const setCardSize = ({ width }) {
    setState({
      containerWidth: width > 0 ? width : defaultWidth
    },() => { 
      setState({numCols: Math.max(Math.floor(state.containerWidth / baseThumbnailWidth), 1) }, () => {
        setState({numRows: Math.ceil(sheetList.size / state.numCols)}, () => {
          setState({cardWidth: Math.floor(state.containerWidth / state.numCols - 2 * marginBetweenCards)}, () => {
            setState({cardHeight: Math.round(state.cardWidth * thumbnailProportion)});
          });
        });
      });
    });
  }

Ideally I would like the containerWidth variable to update first, then the numCols variable, then the cardWidth, then the cardHeight. Is there any way to do this synchronously so I don't get an undefined value?

2

2 Answers

2
votes

I'm sort of confused on what you want to achieve. But don't forget, unlike a class you have to set all properties a in state each time.

setState({numCols: Math.max(Math.floor(state.containerWidth / baseThumbnailWidth), 1) }

This code will replace all your state with just numCols. You want the rest of state in there like this, now only numCols will change, everything else will be the same.

setState({...state, numCols: Math.max(Math.floor(state.containerWidth / baseThumbnailWidth), 1) }

Next thing to remember is if you want to change state multiple time in one render use this form:

setState(oldState => {...oldState, newValue: 'newValue'});

This will allow for multiple updates to state in one render with the last value set instead of on the last render. For example:

const [state, setState] = useState(0); // -> closed on this state's value!

setState(state + 1);
setState(state + 1);
setState(state + 1); //State is still 1 on next render 
// because it is using the state which happened on the last render.
// When this function was made it "closed" around the value 0
// (or the last render's number) hence the term closure.

vs this:

const [state, setState] = useState(0);

setState(state => state + 1);
setState(state => state + 1);
setState(state => state + 1); //State is 3 on next render.

But why not just calculate the values synchronously?

  const setCardSize = (width) => {
    const containerWidth = width > 0 ? width : defaultWidth;
    const numCols = Math.max(Math.floor(containerWidth / baseThumbnailWidth), 1);
    const numRows = Math.ceil(sheetList.size / numCols);
    const cardWidth = Math.floor(containerWidth / numCols - 2 * marginBetweenCards);
    const cardHeight = Math.round(cardWidth * thumbnailProportion);
    setState({containerWidth, numCols, numRows, cardWidth, cardHeight});
  }

Check out the docs it discusses

Unlike the setState method found in class components, useState does not automatically merge update objects.

If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value. Here’s an example of a counter component that uses both forms of setState:

2
votes

Seeing as you're calculating a load of variables that are dependant upon another, and you want the state to all update at the same time, why not split the calculations out and set state once at the end? Much more readable, and only need one setState call.

 const setCardSize = ({ width }) => {
    const containerWidth = width > 0 ? width : defaultWidth;
    const numCols = Math.max(Math.floor(containerWidth / baseThumbnailWidth), 1,);
    const numRows = Math.ceil(sheetList.size / numCols);
    const cardWidth = Math.floor(containerWidth / numCols - 2 * marginBetweenCards);
    const cardHeight = Math.round(cardWidth * thumbnailProportion);
    setState({ containerWidth, numCols, numRows, cardWidth, cardHeight });
  };

To answer the actual question though, if you want to cause the state update of one variable to immediately (or "synchronously" as you put it) update another state variable, then use useEffect.

You just give useEffect two parameters: a function to run every time a dependant variable changes, and then an array of those variables to keep an eye on.

It is cleaner (and faster, less bug-prone, and generally recommended for functional components) for each state variable to have its own useState, rather than just one large object, which I have also done here.

  const [containerWidth, setContainerWidth] = useState(0);
  const [numCols, setNumCols] = useState(0);
  const [numRows, setNumRows] = useState(0);
  const [cardWidth, setCardWidth] = useState(0);
  const [cardHeight, setCardHeight] = useState(0);

  const setCardSize = ({ width }) => setContainerWidth(width > 0 ? width : defaultWidth)
  useEffect(() => setNumCols(Math.max(Math.floor(containerWidth / baseThumbnailWidth), 1)) , [containerWidth])
  useEffect(() => setNumRows(Math.ceil(sheetList.size / numCols)), [numCols])
  useEffect(() => setCardWidth(Math.floor(containerWidth / numCols - 2 * marginBetweenCards)), [containerWidth])
  useEffect(() => setCardHeight(Math.round(cardWidth * thumbnailProportion)), [cardWidth])