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:
- 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
- the
left
button, which should walk backward the loop doesn’t work. It is broken completely. the right
button works though. Not sure why.
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>
);
}