0

I have two objects that I'm trying to loop through using an increment and decrement function. I have the variables being stored in localStorage and the expectation is that no matter which value has been selected last, upon refresh or reload, the counter should still work. However, I've experienced some unexpected occurrences upon using the increment and decrement functions where the variable does not line with the index of the object as shown by the reducer state.

  const increment = (props) => {
    return (props.state + 1) % props.length;
  };

  const decrement = (props) => {
    return (props.state - 1 + props.length) % props.length;
  };

  const colors = [
    { id: 0, type: "Red" },
    { id: 1, type: "Blue" },
    { id: 2, type: "Green" },
    { id: 3, type: "Yellow" }
  ];

For example, at times when I call the state, it will tell me that the color is Yellow and the index is 2, and is generally inconsistent. I've tried storing the counter variable within localStorage and calling from that in hopes that it will synchronize the counter with the intended variable, however, this has not worked.

Here is a demo with CodeSandbox. I'm still relatively new to React and I'm not sure if using a counter is the best method for this problem.

Here is a reproducible example of the output I've received (upon clearing localStorage and refreshing the app) and the expected output for the shapes.

Output: Square (next) Square (previous) Circle (previous) Square (previous) Octagon (next) Triangle

Expected output: Square (next) Circle (previous) Square (previous) Octagon (previous) Triangle (next) Octagon

paq
  • 87
  • 8

2 Answers2

1

Having forked and refactored your original sandbox here there were a few challenges.

Firstly, the reducer actions were expanded to include increment and decrement. The example provided in React Hooks Reference has an example of incrementing and decrementing, though reducers are not just applicable for counting. The dispatch of your reducer will set the state, so having Action.Set is redundant.

Let's take a look at one of the original buttons' onClick methods. Originally, colorCount was being decremented and then an update to color occurs based on the state at the time of click, not when the state is updated. To visualize this on the original demo, log the state before and after setData.

onClick={() => {
  setData({
    payload: decrement({
      state: state.colorCount,
      length: colors.length
    }),
    name: "colorCount"
  });
  setData({
    payload: colors[state.colorCount].type,
    name: "color"
  });
}}

Now, the same onClick calls the decrement method.

onClick={() => {
  decrement({
    name: "colorCount"
  });
}}

The decrement method, moved to the context, just calls the dispatch with proper type and payload containing the name of the value to update.

const decrement = (payload) => {
  dispatch({
    type: ACTIONS.DECREMENT,
    payload
  });
};

Lastly, the reducer updates the states colorCount paired with its prefix color and shapeCount paired with its prefix shape

const reducer = (state, action) => {
  // Verify which value we need to update along with its count
  const isColor = action.payload.name.includes("color");
  // Select the proper option in context
  const options = isColor ? colors : shapes;

  switch (action.type) {
    case ACTIONS.INCREMENT:
      // Increment the count for use in the value and type setting
      const incrementedCount =
        (state[action.payload.name] + 1) % options.length;
      return {
        ...state,
        // Set the new count
        [action.payload.name]: incrementedCount,
        // Set the new color or shape type
        [isColor ? "color" : "shape"]: options[incrementedCount].type
      };
    case ACTIONS.DECREMENT:
      // Decrement the count for use in the value and type setting
      const decrementedCount =
        (state[action.payload.name] - 1 + options.length) % options.length;
      return {
        ...state,
        // Set the new count
        [action.payload.name]: decrementedCount,
        // Set the new color or shape type
        [isColor ? "color" : "shape"]: options[decrementedCount].type
      };
    default:
      return state;
  }
};

As far as updating the localStorage on update of a value, the easiest way is another useEffect dependent on the state values. Feel free to update the localStorage how and when you want, but for the purposes of keeping the state on reload the simplest approach was kept.

  useEffect(() => {
    localStorage.setItem("colorCount", JSON.stringify(state.colorCount));
    localStorage.setItem("color", JSON.stringify(state.color));
    localStorage.setItem("shapeCount", JSON.stringify(state.shapeCount));
    localStorage.setItem("shape", JSON.stringify(state.shape));
  }, [state.colorCount, state.color, state.shape, state.shapeCount]);

To the point made about contexts, this counter example does benefit from simplicity. The reducer can be used all within the App. Contexts are best used when passing down props to children becomes cumbersome.

TheOverwatcher
  • 126
  • 1
  • 5
  • Thanks for taking the time to give such a detailed response! You've simplified the original demo by a lot and the comments are very helpful. Is there ever a case where it's okay to have state-consuming functions outside of the reducer? – paq Sep 28 '22 at 15:25
  • 1
    I'm not sure I understand what you mean by state-consuming functions. An example use case or clarification would be preferable, but... There are many ways to track component state, but `useReducer`'s dispatch is common to reduce complex and reused logic. There is also `useState(someFunc)` where `someFunc` is a method that returns a value for the state which can be modified before the return. – TheOverwatcher Sep 28 '22 at 16:49
0

What you've provided here is not sufficient for a minimum reproducible example. We can't offer much help if your problem is only happening "at times" -- please provide specific cases of what steps you take to obtain a specific problem.

Generally speaking, I think it might simplify your code to use two separate state variables. Context seems like overkill for this use case.

const [colorIdx, setColorIdx] = useState(0);
const [shapeIdx, setShapeIdx] = useState(0);

And, as a style note, it is usually a good idea to avoid inline function definitions. The following is much more readable, for example:

const incrementColorIdx = () => {
  setColorIdx((colorIdx + 1) % colors.length);
}
...

<button onClick={incrementColorIdx}>Next</button>
Nate Norris
  • 836
  • 7
  • 14
  • Thank you for your response. I've modified my original post to add a reproducible example of the output and the expected output. I've decided to use context as I have a lot more than two states that I'm managing but I've reduced my code to the demo I provided to illustrate the problem I've encountered. – paq Sep 27 '22 at 20:36