react-spring icon indicating copy to clipboard operation
react-spring copied to clipboard

Animated component breaks formatted string

Open alextes opened this issue 2 years ago • 12 comments

🐛 Bug Report

I have an <animated.p>. It gets passed a formatted string from Intl.NumberFormat with signDisplay: "always". This means a + is prepended. This plus gets dropped by the component 🙈 .

I've logged the strings as they get passed, it never omits the plus. Yet, after the animation is finished, the component with its child string gets rerendered for whatever reason it seems, and the plus disappears.

I'm pretty stumped. As it stays on the minus side I was thinking maybe it gets parsed, recognized as number, re-stringified, and that's how it disappears. Has to be something like it, and yet, even prepending a letter to the passed string to break any presumed parsing doesn't change anything.

To Reproduce

  • create an animated component with a spring that animates to a positive number.
  • pass the positive number formatted with NumberFormat signDisplay: always

Expected behavior

strings stay formatted the way they're passed.

Link to repro (highly encouraged)

https://codesandbox.io/s/happy-breeze-k5b37?file=/src/App.js

If you'd like to skip the repro, here's what the following logs:

<animated.p className="-mb-2">
{growthRateA.to((n) => {
  console.log(
    "gauge val",
    growthRate,
    n,
    percentChangeFormatter.format(n)
  );
  return `${percentChangeFormatter.format(n)}`;
})}
Details
gauge val -0.011 -0.011 -1.1%
gauge val 0.031 -0.011 -1.1%
gauge val 0.031 -0.010121548193517607 -1.0%
gauge val 0.031 -0.00857916774249765 -0.9%
gauge val 0.031 -0.005950740117543118 -0.6%
gauge val 0.031 -0.0029392530728557277 -0.3%
gauge val 0.031 0.0030268882813645867 +0.3%
gauge val 0.031 0.0043572052262135326 +0.4%
gauge val 0.031 0.008124811754838784 +0.8%
gauge val 0.031 0.012428674047926122 +1.2%
gauge val 0.031 0.015282572633774496 +1.5%
gauge val 0.031 0.019405160906445458 +1.9%
gauge val 0.031 0.022080519063728186 +2.2%
gauge val 0.031 0.02389470716540138 +2.4%
gauge val 0.031 0.026299391624859 +2.6%
gauge val 0.031 0.028825197159883555 +2.9%
gauge val 0.031 0.030074829158552547 +3.0%
gauge val 0.031 0.031120467901732316 +3.1%
gauge val 0.031 0.032136375095277364 +3.2%
gauge val 0.031 0.03324458904176794 +3.3%
gauge val 0.031 0.03341674944892716 +3.3%
gauge val 0.031 0.03379429059057049 +3.4%
gauge val 0.031 0.034034462926820684 +3.4%
gauge val 0.031 0.03402336228008903 +3.4%
gauge val 0.031 0.033967395783605976 +3.4%
gauge val 0.031 0.0338198482844161 +3.4%
gauge val 0.031 0.033655547240881566 +3.4%
gauge val 0.031 0.03323786042677272 +3.3%
gauge val 0.031 0.033175304905596906 +3.3%
gauge val 0.031 0.032903915378158954 +3.3%
gauge val 0.031 0.03263141011617155 +3.3%
gauge val 0.031 0.03235126611329709 +3.2%
gauge val 0.031 0.03210147120371028 +3.2%
gauge val 0.031 0.03187080596101644 +3.2%
gauge val 0.031 0.03166233598676437 +3.2%
gauge val 0.031 0.031467693381792144 +3.1%
gauge val 0.031 0.03130912302727817 +3.1%
gauge val 0.031 0.031174775466065223 +3.1%
gauge val 0.031 0.031057804428564314 +3.1%
gauge val 0.031 0.030965058583005904 +3.1%
gauge val 0.031 0.030901156456320494 +3.1%
gauge val 0.031 0.03085035087798566 +3.1%
gauge val 0.031 0.030813362199849245 +3.1%
gauge val 0.031 0.030792923903493395 +3.1%
gauge val 0.031 0.030781616531037656 +3.1%
gauge val 0.031 0.03078008319142252 +3.1%
gauge val 0.031 0.030785416615148938 +3.1%
gauge val 0.031 0.03079703394264702 +3.1%
gauge val 0.031 0.03081304343731894 +3.1%
gauge val 0.031 0.030829713834723847 +3.1%
gauge val 0.031 0.030848873417013662 +3.1%
gauge val 0.031 0.030870956731571284 +3.1%
gauge val 0.031 0.030892771279615535 +3.1%
gauge val 0.031 0.030912490436875762 +3.1%
gauge val 0.031 0.03093465114884642 +3.1%
gauge val 0.031 0.0309566119187836 +3.1%
gauge val 0.031 0.031 +3.1%
gauge val 0.031 0.031 +3.1%
gauge val 0.031 0.031 +3.1%

Kapture 2021-08-16 at 09 14 31

Environment

  • react-spring v9.2.4
  • react v17.0.x

Thanks for an amazing animation library! Managed to drop a full component. So easy to use 👍 .

alextes avatar Aug 16 '21 08:08 alextes

aha, this library can animate strings with numbers, even something like rgb(0,100,0). Well, that explains. Definitely parsing the number out of the string and updating with what is thought to be equivalent but isn't.

alextes avatar Aug 16 '21 08:08 alextes

Woohooo, found a workaround 🙌 .

I doubt anyone else is troubled by this haha, but after days and days of incredibly hard work I'm v. glad the next version of https://ultrasound.money can go live without replacing react-spring for this component (the main candidate had trouble too).

I'm now boolean flagging whether we are resting or not (onRest / onStart) and replacing the animated component with a frozen one. That still means you see a flicker as the non-animated value updates immediately but luckily!! we have animatedValue.get() in this library to get around that 🙏 .

alextes avatar Aug 16 '21 08:08 alextes

I have a similar issue with formatted currencies. In my case I'm using shopify's i18n package to format a currency. When switching to german formatting, 0s get dropped.

https://codesandbox.io/s/happy-shape-gfpjw?file=/src/App.js

(change locale = 'en' to local = 'de' and you'll notice 6.920,60 turns into 6.92,6 (last number is the one showcasing the issue). The ones above I just added to verify the formatCurrency method works as expected.

philschoefer avatar Jan 06 '22 15:01 philschoefer

Hi @philschoefer did you solved your issue? Because i'm having exactly the same problem :/

Globix avatar Mar 21 '23 00:03 Globix

Having the same issue as @philschoefer

For example, I have a number with a zero: 12350532 I then format it to Turkish locale string:

const [prevTotal, setPrevtotal] = useState(0);

const total = calculateTotal(values)

useEffect(() => {
    setPrevtotal(total);
  }, [total, prevTotal]);

const props = useSpring({ val: total, from: { val: prevTotal } });

return (
 <animated.p>{props.val.to((val) => val.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }))}</animated.p>
)

The formatted number that gets displayed is: 12.35.532,00 but it should be 12.350.532,00 The problem does not occur when using "en-US" formatting or when I simply render the value without react-spring

alperaktas-ets avatar Apr 06 '23 07:04 alperaktas-ets

To try and move things in a helpful direction, if someone is interested in tackling this bug, my uninformed guess would be that it wouldn't be very hard to pass some boolean to a spring that says don't try to parse and display my string, after formatting is done, don't touch it.

alextes avatar Apr 06 '23 07:04 alextes

I think we could look at adding a new method to the SpringValue e.g. .print which does not return an interpolation and therefore the string shouldn't need to be touched afterwards...

EDIT: to be clear, i don't consider this a bug, however I do understand the pain point.

joshuaellis avatar Apr 09 '23 08:04 joshuaellis

I think this is a bug, and to justify that understanding, I'll point out that only the stopped values are like this. If react-spring is correctly calling the function passed to to() for the tweens, then it makes even less sense that the spring will munge it on the keyframes. I would have expected quite the opposite - that the spring might get the keyframes from the function you're given and then try to tween between them yourself.

Indeed, even that surprised me - when passing a function to transform the values of an interpolation, I don't expect the interpolation to re-interpolate the results of that function. I expect that now I'm taking matters into my own hands, and it's my responsibility to do so efficiently enough that I can run it on every frame, and to format it in a way that is accepted by whatever I'm passing the interpolation to.

I also don't expect react-spring to auto-animate between CSS values at all, at least not without me telling it I need it to do so. The documentation does not make it obvious that react-spring will do this, only using a few examples to suggest that it is capable of animating them without explaining how it does this. This I would agree is a painfully magical feature and not a bug. It can pick up color words and convert them to their colors and then animate between them! Awesome! ... Except I had no idea it could do that, so I wouldn't have tried it. Honestly, I expected this library to leave that behavior to CSS itself. So I think there's a documentation component that needs solving as well.

I may take a crack at this later; for now, I'll find some form of workaround.

programmablereya avatar May 06 '23 03:05 programmablereya

The workaround is trivial - and, I suspect, so too shall the fix be.

import {Interpolation, InterpolatorArgs, Globals} from "@react-spring/web";
import {getAnimated} from "@react-spring/animated";

export class FixedInterpolation<Input = any, Output = any> extends Interpolation<Input, Output> {
    constructor(readonly source: unknown, args: InterpolatorArgs<Input, Output>) {
        super(source, args);
        getAnimated(this)!.setValue(this._get())
    }
}

Globals.assign({to: (source, args) => new FixedInterpolation(source, args)})

Because the AnimatedString's value is not set at first, the AnimatedString tries to interpolate what it might be from two of the same value and a number. By setting its value, the interpolation is bypassed and so too is the bug.

A more principled fix might require more knowledge of the code than I have. But I remain firm that this is a bug.

programmablereya avatar May 06 '23 03:05 programmablereya

Regardless of what it's been triaged as, if you think you have a solution, feel free to submit a PR to fix it 😊

joshuaellis avatar May 06 '23 06:05 joshuaellis

My apologies - last night I was frustrated and overtired, and I had been wrestling with this problem for far too long before pinning it down to this library (most of my bugs were my own fault), so it came out harsher than necessary. u_u

Today I'm busy, but I'll put together a PR with a fix and some tests to confirm that it a) works and b) doesn't break anything else tomorrow, most likely!

Thanks! :)

programmablereya avatar May 06 '23 17:05 programmablereya

@programmablereya any updates on this? I just ran into this issue, and your fix worked for me (thanks!). If you're not interested, I'm happy to submit a PR for your fix or see if I can find any alternative solutions.

arcturus3 avatar Aug 26 '23 17:08 arcturus3