4

inside my react app, users are able to enter data that is saved on the server. The data is saved immediatly, users don't have to press any "save" button. I want to display a short animation (similar to https://codepen.io/Sixclones/pen/VBdeXL) whenever I'm sending data to the server. After doing some research, I figured out that I should use react-transition-group (http://reactcommunity.org/react-transition-group/css-transition).

I made a component savingIcon:

const SavingIcon: React.SFC<SavingIconProps> = ({
    className,
    saving,
}) => {

    return (
        <div className={ classNames(className, "saving-icon") }>
            <CSSTransition 
                timeout={ 200 }
                classNames="saving"
                in={ saving }
            >

                <div className="saving-balls">
                    <div className="saving-balls__item" />
                    <div className="saving-balls__item" />
                    <div className="saving-balls__item" />
                </div>
            </CSSTransition>
        </div>
    );
}

Whenever something is being saved, saving is set to true and I'd like to display the animation.

Inside saving-icon.scss I put the following CSS (I tried out several approaches, therefore there might be some unnecessary css):

@keyframes bouncing {

    0% {
        transform: translate3d(0, 10px, 0) scale(1.2, 0.85);
    }

    100% {
        transform: translate3d(0, -20px, 0) scale(0.9, 1.1);
    }
}

div.saving-icon {
    position: sticky;
    top: 15vh;
    display: flex;
    flex-direction: column;
    justify-content: end;
    height: 84vh;

    $anim-drt: 0.4s;
    $anim-ease: cubic-bezier(.6, .05, .15, .95);

    .saving-enter {

        div.saving-balls{

            &__item {
                background-color: $success-color;
                transition: background-color 20ms;  

            }
        }
    }

    .saving-enter-active {       

        div.saving-balls{

            &__item {
                background-color: $active-color;
                transition: background-color 20ms;  

            }

            &:nth-child(1) {
                animation: bouncing $anim-drt alternate infinite $anim-ease;
                transition: animation 1000ms;  
            }

            &:nth-child(2) {
                animation: bouncing $anim-drt $anim-drt/4 alternate infinite 
                    $anim-ease backwards;
                transition: animation 1000ms;  
            }

            &:nth-child(3) {
                animation: bouncing $anim-drt $anim-drt/2 alternate infinite 
                    $anim-ease backwards;
                transition: animation 1000ms;  
            }
        }
    }

    .saving-exit {
        div.saving-balls{

            &__item {
                background-color: $active-color;
                transition: background-color 20ms;  

            }

            &:nth-child(1) {
                animation: bouncing $anim-drt alternate infinite $anim-ease;
                transition: animation 1000ms;  
            }

            &:nth-child(2) {
                animation: bouncing $anim-drt $anim-drt/4 alternate infinite 
                    $anim-ease backwards;
                transition: animation 1000ms;  
            }

            &:nth-child(3) {
                animation: bouncing $anim-drt $anim-drt/2 alternate infinite 
                    $anim-ease backwards;
                transition: animation 1000ms;  
            }
        }
    }

    .saving-exit-active {
        div.saving-balls{

            &__item {
                background-color: $success-color;
                transition: background-color 20ms;  
                transition: animation 1000ms;  
            }


        }     
    }

    div.saving-balls {
        width: 3em;
        height: 10vh;

        z-index: 5;

        display: flex;
        justify-content: space-between;
        align-items: center;

        &__item {
            width: 0.7em;
            height: 0.7em;
            border-radius: 50%;
            background: $success-color;
        }
    }

}

My issue is the following: Usually saving something only takes very short time; not enough time to actually run the animation once. My balls just become red for a split second and return being green (active and success color are some type or red and green). I would like to have the animation running a longer period of time, something around 2 seconds. I tried some hacks using effects, states and timeouts, but they didn't work well and I'd rather use a correct solution instead of some dirty hack.

I'm not very familiar with css transitions and animations, neither with react-transition-group. I hope for some existing easy way to play an animation for a certain minimum amount of time (if the connection is weak, the animation should show until the data is saved).

As an alternative, I accept different suggestions on how to tell the user that his input has been saved instead of an animation at the corner of the page. Currently, everything the user enters is saved, but he might not notice that this happened and search for a "save" button.

schadenfreude
  • 212
  • 4
  • 15

1 Answers1

1

For anybody with a similar issue: I solved the issue by using useState and only changing the state value for saving if it indeed changed. I didn't use a transition group.

The working saving component:

const SavingIcon: React.SFC<SavingIconProps> = ({
    className,
    saving,
}) => {

    const [ isSaving, setIsSaving ] = useState(false);

    useEffect(() => {
        if (isSaving !== saving) setIsSaving(saving);
    }, [ saving ]);

    return (
        <div className={ classNames(className, "saving-icon", {
            "saving-active": isSaving
        }) }>
            <div className="saving-balls">
                <div className="saving-balls__item" />
                <div className="saving-balls__item" />
                <div className="saving-balls__item" />
            </div>
        </div>
    );
}

scss:

@keyframes bouncing {

    0% {
        transform: translate3d(0, 10px, 0) scale(1.2, 0.85);
    }

    100% {
        transform: translate3d(0, -20px, 0) scale(0.9, 1.1);
    }
}

div.saving-icon {
    position: fixed;
    top: 86px;
    right: 1.3vw;

    $anim-drt: 0.4s;  /* duration */
    $anim-ease: cubic-bezier(.6, .05, .15, .95);

    &.saving-active {

        div.saving-balls {

            div.saving-balls__item {
                background-color: $active-color;
                transition: background-color 20ms;

                &:nth-child(1) {
                    animation: bouncing $anim-drt alternate infinite $anim-ease;
                }

                &:nth-child(2) {
                    animation: bouncing $anim-drt $anim-drt/4 alternate infinite
                        $anim-ease backwards;
                }

                &:nth-child(3) {
                    animation: bouncing $anim-drt $anim-drt/2 alternate infinite
                        $anim-ease backwards;
                }
            }
        }
    }

    div.saving-balls {
        width: 3em;
        height: 30px;

        z-index: 5;

        display: flex;
        justify-content: space-between;
        align-items: center;

        &__item {
            width: 0.7em;
            height: 0.7em;
            border-radius: 50%;
            background: $success-color;
        }
    }

}
schadenfreude
  • 212
  • 4
  • 15
  • I did something similar, but it causes / caused a double render of the component so a flicker - my stakeholder isn't keen on that. Wondering if you found another pattern? In mine, the exit / exiting state is overwritten by the entered state so that animation is not shown - painful! – Jeremy Mar 29 '23 at 13:31
  • 1
    It has been some time. If I remember correctly, I debounced the call which updates the SavingIcon component. If you don't know what debouncing is, you may search for it; brief summary: it wraps a function so that if you call it multiple times in a certain time frame, it only passes one call. That might solve your problem? – schadenfreude Mar 30 '23 at 15:53