React: make a cycle-through component with both forward and backward directions using Generator

275 views Asked by At

I have an array of strings to display const array = ["one", "two", "three"]; .

The UI initially shows the first item in the array i.e. "one". From there I have a button right when clicked it shows the next item or string which is two, and then three, after three it should go back to one and start from there again. I also have a left button, when clicked it shows the previous item or string, if the current string is two, the previous string is one, and then after one it starts from three and walks backward.

I am using generator to do it. Here is my attempt


function* stepGen(steps) {
  let index = 0;
  while (true) {
    const direction = yield steps[index];
    index = (index + (direction === "forward" ? 1 : -1)) % steps.length;
  }
}

const array = ["one", "two", "three"];
let gen = stepGen(array);
const getNext = () => gen.next("forward").value;
const getPrev = () => gen.next("backward").value;

export default function App() {
  const [current, setCurrent] = useState(() => getNext());
  const onRight = () => {
    const next = getNext();
    setCurrent(next);
  };
  const onLeft = () => {
    const prev = getPrev();
    setCurrent(prev);
  };

  return (
    <div className="App">
      <h1>{current}</h1>
      <button onClick={onLeft}>left</button>
      <button onClick={onRight}>right</button>
    </div>
  );
}


Here is a live demo you can play with https://codesandbox.io/s/cyclethrough1-deh8p?file=/src/App.js

Apparently the current behavior is buggy. There are multiple issues that I don't know the causes and the solutions:

  1. the UI starts with two not one. I guess it has something to do with how I initiate my state current
const [current, setCurrent] = useState(() => getNext());

I thought () => getNext() is only to get called once when the component first mounts so current should be one from the start.

And I tried to initiated the state with

const [current, setCurrent] = useState(array[0]);

It indeed starts with the first item in the array which is one but you have to click right button twice to make it go to two. Here is the live demo for this variation https://codesandbox.io/s/cyclethrough2-5gews?file=/src/App.js

  1. the left button, which should walk backward the loop doesn't work. It is broken completely. the right button works though. Not sure why.
4

There are 4 answers

0
Ori Drori On BEST ANSWER

The problem with getPrev is the remainder (%) operator, which unlike the modulo operation returns a negative result when the remainder is negative. To solve that use a modulo function instead:

// modulo function
const mod = (n, r) => ((n % r) + r) % r;

To solve the problem on the 1st render create the initial value outside of the component. This is a workaround, since I can't find the reason for that bug.

const init = getNext(); // get the initial value

export default function App() {
  const [current, setCurrent] = useState(init); // use init value

I would also save the need for a ternary to determine the increment by passing 1 and -1 in getNext and getPrev respectively.

Full code example (sandbox):

// modulo function
const mod = (n, r) => ((n % r) + r) % r;

function* stepGen(steps) {
  let index = 0;
  while (true) {
    const dir = yield steps[index];
    index = mod(index + dir, steps.length); // use mod function instead of remainder operator
  }
}

const array = ['one', 'two', 'three'];
const gen = stepGen(array);

const getPrev = () => gen.next(-1).value; // dec directly
const getNext = () => gen.next(1).value; // inc directly

const init = getNext(); // get the initial value

export default function App() {
  const [current, setCurrent] = useState(init); // use init value

  const onLeft = () => {
    const next = getPrev();
    setCurrent(next);
  };

  const onRight = () => {
    const prev = getNext();
    setCurrent(prev);
  };

  return (
    <div className="App">
      <h1>{current}</h1>
      <button onClick={onLeft}>left</button>
      <button onClick={onRight}>right</button>
    </div>
  );
}
0
Joji On

The problem was indeed with the remainder (%) operator, and use const mod = (n, r) => ((n % r) + r) % r; instead would fix it.

I wanted to answer my own questions because I think I understand what were causing the weird issues with the initial state.

  1. For the issue with the UI starting with two not one, it was caused by the double rendering of React's strict mode.
  2. And then if we use const [current, setCurrent] = useState(array[0]); instead, there would be another problem where you need to double click on right to make it go to two. This is because in the generator, index starts at 0, so after first click, the iterator would yield the current index which is 0 not 1, therefore we need to double click it to move forward.
0
codeKarma On

Use the logging of the js like window.log() enter image description hereIt is clearly visible that the bug is after clicking left the number generated are -2 -1 which are not array indexes so one idea can be to make them positive.But the first step is to find the bug

2
Olek Oliynyk On

So, my proposed answer is:

  1. For one, as one of the answers has mentioned, it is caused by a double render. My proposal is to not initialise the value in useState but in the useEffect hook with empty dependencies. This will cause it to run only once on the first render. Example:
  useEffect(() => {
    setCurrent(() => getNext())
  }, [])

  1. The bug is caused by the fact that you are subtracting 1 from the index. When its in the position 0, it leads to index of -1 which is does not yield an item. A simple solution is to loop the index back to the last element if its negative.
function* stepGen(steps) {
  let index = 0;
  while (true) {
    const direction = yield steps[index];
    index = (index + (direction === "forward" ? 1 : -1)) % steps.length;
    if (index < 0) {
      index = steps.length - 1;
    }
  }