0

I'm trying to create a small component that will fade between it's children when one of its methods is called. I've been following this code, but so it supports any number of children. So far, I have this:

export const Fader = React.forwardRef<FaderProps, Props>((props, ref) => {
    const children = React.Children.toArray(props.children);

    const [currentChild, setCurrentChild] = useState({
        child: props.startIndex || 0,
        direction: 1,
    });
    let nextChild = 0;

    const fadeNext = (): void => {
        queueNextFade(); //Queues the next fade which fades in the next child after the current child has faded out

        setCurrentChild({
            child: currentChild.child,
            direction: +!currentChild.direction,
        });
        nextChild = currentChild.child + 1;
    }

    const fadePrev = (): void => {
    }

    const fadeTo = (index: number): void => {

    }

    const queueNextFade = (): void => {
        setTimeout(() => {
            setCurrentChild({
                child: nextChild,
                direction: +!currentChild.direction,
            });

        }, props.fadeTime || 500)
    }

    useImperativeHandle(ref, () => ({ fadeNext, fadePrev, fadeTo }));

    return (
        <div>
            {
                React.Children.map(children, (child, i) => (
                    <div key={i}
                        style={{
                            opacity: i === currentChild.child ? currentChild.direction : "0",
                            transition: `opacity ${props.fadeTime || 500}ms ease-in`,
                        }}
                    >
                        {child}
                    </div>
                ))
            }
        </div>
    )
});

Logic wise it does work, but what actually happens is that the first child fades out, but the next child doesn't fade in. If faded again, the second child fades in then fades out, and the next child fades in. (View in sandbox)

For a while I was confused as to why that was happening because I'm using the same logic as the other library. I did some research on seeing if I could make useState be instant and I came across this post and I quote from it:

Even if you add a setTimeout the function, though the timeout will run after some time by which the re-render would have happened, the setTimeout will still use the value from its previous closure and not the updated one.

which I realised is what's happening in my situation. I start the setTimeout in which currentChild.direction is 1. Then a state change happens and the direction changes to 0. A long time later the setTimeout finishes but instead of changing the direction from 0 to 1, it changes from 1 to 0 because it kept it original value from when it was first called, hence why the second child doesn't fade in and just stays invisible.

I could just change it to:

let currentChild = {...}

and the have a "blank" useState to act as a forceUpdate but I know force updates are against React's nature and there's probably a better way to do this anyway.

If anyone could help out, I would appreaciate it

DreamingInsanity
  • 167
  • 2
  • 10

1 Answers1

0

For anyone in the future, I found this post explaning that setTimeout will use the value from the initial render of the component To fix this, I changed my setTimeout to:

setTimeout(() => {
    setCurrentChild(currentChild => ({
        child: nextChild,
        direction: +!currentChild.direction,
    }));

}, props.fadeTime || 500)
DreamingInsanity
  • 167
  • 2
  • 10