React-native-svg: Text element wont redraw when SVG size changes

Created on 12 Jun 2018  路  34Comments  路  Source: react-native-svg/react-native-svg

I have a Svg element with dynamic width and height, and positioning of all elements inside of it is related to this elements width and height, however, Text elements won't change their places after width and height changes, everything else is fine.
This problem occurs in 6.3.1 version of react-native-svg, in 5.5.1, it works fine.

Environment:
  OS: Windows 10
  Node: 8.11.2
  Yarn: 1.7.0
  npm: 6.1.0
  Watchman: Not Found
  Xcode: N/A
  Android Studio: Not Found

Packages: (wanted => installed)
  react: 16.3.1 => 16.3.1
  react-native: 0.55.4 => 0.55.4
import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  View,
  Button,
  Alert
} from 'react-native';
import Svg, {G, Circle, Image, Text} from 'react-native-svg';

export default class App extends Component<Props> {
    constructor(props) {
        super(props);
        this.state = {s:400};
    }
  render() {
    return (
      <View style={styles.container}>  
        <View style={{borderWidth:2}}>
            <Svg width={this.state.s} height={this.state.s}>    
                <Circle cx="50%" cy="50%" r={this.state.s/2 - 10} stroke="red" fillOpacity="0"/>
                <Text x={this.state.s/2} y={this.state.s/2} textAnchor="middle" fontWeight="bold" fontSize={this.state.s/25} fill="black">HOGWARTS</Text>
            </Svg>
        </View>
        <Button title="test" onPress={() => {this.setState((this.state.s == 400) ? {s:200}:{s:400})}}/>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  }
});

Sorry if this is duplicate, couldn't find anything related.

Most helpful comment

You should use key={Math.random()} for text element.

<Text x={this.state.s/2} y={this.state.s/2} key={Math.random()} .................>HOGWARTS</Text>

All 34 comments

Any help?

You should use key={Math.random()} for text element.

<Text x={this.state.s/2} y={this.state.s/2} key={Math.random()} .................>HOGWARTS</Text>

Thanks, worked

If you have more than one list like this, you may experience a collision every 1,000,000,000 renders or so. Best to append a list specific string with the list element index if you care to avoid this problem:

<Text x={this.state.s/2} y={this.state.s/2} key={'CollegeList' + i + Math.random()} .................>HOGWARTS</Text>

Having to set a random key to get the text element to re-render seems like an ugly workaround. It goes against the main idea to only re-render what's necessary.

I have a similar problem when moving text elements, i.e. changing the x- and y props of the text. These props does not seem to trigger a re-render so I have to (temporarily) set a random key as @mcihan proposes.

I have the similar problem too. And I find that if I comment some lines of code in elements/Text.js, the re-render works.

render() {
        const props = this.props;

        return (
            <RNSVGText
                ref={ele => {
                    this.root = ele;
                }}
                {...extractProps(
                    {
                        ...props,
                        x: null,
                        y: null
                    },
                    this
                )}
                {...extractText(props, true)}
            />
        );
    }

into

render() {
        const props = this.props;

        return (
            <RNSVGText
                ref={ele => {
                    this.root = ele;
                }}
                {...extractProps(
                    {
                        ...props,
                        // x: null,
                        // y: null
                    },
                    this
                )}
                {...extractText(props, true)}
            />
        );
    }

Even though the position of text seems not correct.

They released many versions after I posted this, I don't know why they won't fix this, not even an official response.

Well, there no official support from any company or person, what would you consider official in this case? This is just a community of devs scratching their own itches. Please feel free to attempt fixing the issue you're experiencing. I haven't seen any pr to fix this as of yet, nor do i have any solution readily available. I'm not sure if the proposed fix actually solves the underlying issue, have you tried it?

I have a hypothesis for the underlying reason here, which is caching of the width/height values. If correct, then a clearing of the cache and invalidation of the tree would fix the issue, without requiring a complete reconstruction of the nodes using the change of key. Alternatively just removing the caching as the performance impact hasn't even been properly validated using measurements, might not even be a measurable difference.
Could someone try commenting out these lines:
https://github.com/react-native-community/react-native-svg/blob/master/android/src/main/java/com/horcrux/svg/VirtualNode.java#L309-L311
and these lines:
https://github.com/react-native-community/react-native-svg/blob/master/android/src/main/java/com/horcrux/svg/VirtualNode.java#L323-L325
to see if this is sufficient to eliminate the symptoms?

I've found that wrapping the text in a TSpan and setting the x and y values on that instead of on the Text element seems to workaround this issue properly. Not sure why it doesn't work on Text correctly yet. But just for reference, this works:

import React, { Component } from 'react';
import { StyleSheet, View, Button } from 'react-native';
import { Svg, Circle, Text, TSpan } from 'react-native-svg';

export default class App extends Component {
  state = { s: 400 };
  render() {
    const { s } = this.state;
    return (
      <View style={styles.container}>
        <View style={{ borderWidth: 2 }}>
          <Svg width={s} height={s}>
            <Circle
              cx="50%"
              cy="50%"
              r={s / 2 - 10}
              stroke="red"
              fillOpacity="0"
            />
            <Text
              textAnchor="middle"
              fontWeight="bold"
              fontSize={s / 25}
              fill="black"
            >
              <TSpan x={s / 2} y={s / 2}>
                HOGWARTS
              </TSpan>
            </Text>
          </Svg>
        </View>
        <Button
          title="test"
          onPress={() => {
            this.setState({ s: s === 400 ? 200 : 400 });
          }}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
});

Or, even better, use SVG as it's meant to, and set a viewBox attribute to define your coordinate space, and define resolution independent vector data, as in e.g.

export default class App extends Component {
  state = { s: 400 };
  render() {
    const { s } = this.state;
    return (
      <View style={styles.container}>
        <View style={{ borderWidth: 2 }}>
          <Svg width={s} height={s} viewBox="0 0 100 100">
            <Circle
              cx="50"
              cy="50"
              r="40"
              stroke="red"
              fillOpacity="0"
            />
            <Text
              textAnchor="middle"
              fontWeight="bold"
              fontSize="4"
              fill="black"
            >
              <TSpan x="50" y="50">
                HOGWARTS
              </TSpan>
            </Text>
          </Svg>
        </View>
        <Button
          title="test"
          onPress={() => {
            this.setState({ s: s === 400 ? 200 : 400 });
          }}
        />
      </View>
    );
  }
}

This way the content wouldn't event need any diffing, except for the width and height attribute.

@msand Do you have anything you'd like me to try out on iOS, similar to the code-lines you wanted remove in Java in your above post? I'm building for iOS only at the moment, so I can't help you out with the Java test.

Btw, I couldn't get the TSpan trick to work. Didn't put that much time into it, but I couldn't even get the text to render in the correct position. It might have to do with the transformation I'm doing on both the wrapping Text element and the outer wrapping Group element.

@msageryd Can you try using msand/react-native-svg#ce602c1 ? At least the tspan version seems to work in iOS with that one.

Another option might be to make the key for the text include the values of (specifically) the problematic dynamic parameters, so it only reconstructs the node if any of the relevant props change and doesn't have any non-determinism causing re-renders without actual changes.

I installed your version, restarted the packager as well in case you'd changed something at the native side.

Both my problems persists, i.e.:

  • Text elements are not rerendered when x/y props are changed
  • Matrix transformed Text elements seems to have x and y mixed up (when x/y changes)

I'll give the TSpan another go. I'm not sure what x and y I should put into the TSpan. My wrapping Text element already has x and y as well as a scale transform.

If I wrap the text string in a TSpan without applying x and y props to the TSpan, the text is rendered at the correct place. But the Text does not rerender when x/y changes on the wrapping Text.

I suppose that your workaround needs the x and y props to be applied to the TSpan and those props are what should be altered. Correct? If I move x, y and transform from the outer Text to the inner TSpan nothing seems to change. No rerendering when x and y changes.

Here is my code. It's straight cut and pasted so I wouldn't mess upp any vital information to you. I.e. You'll have to wade through some irrelevant constants etc.

    <SvgText
      textAnchor={'middle'}
      fontSize={fontSize * VIRTUAL_TEXT_SCALE}
      fill={ghostedProps ? ghostedProps.textColor : controlpointLevel.text}
      strokeDasharray={strokeDashArray}
    >
      <TSpan
        transform={{ scale: 1 / VIRTUAL_TEXT_SCALE }}
        x={this.props.labelCenter.x * VIRTUAL_TEXT_SCALE}
        y={(this.props.labelCenter.y + labelVerticalAdjustment) * VIRTUAL_TEXT_SCALE}
      >
        {this.props.labelText}
      </TSpan>
    </SvgText>

The above plus some other Svg elements is wrapped into a G element which in turn is scaled and translated with setNativeProps({matrix: ..})

Hmm. I'm not sure what happened now. Maybe my first attempt to install ce602c1 went wrong due to some build cache or something. After restarting the packager and the app again, someething went wrong. The app didn't even start.

I got an error stating that the NSString '12' could not be converted to an YGValue. 12 is a quite common fontSize in my app, but after searching everywhere I cannot find any place where I'm applying the fontsize as a string.

You need to rebuild the native code as well, restarting the packager isn't enough. Can you rebuild and try again?

How do I rebuild? Should I do a "build" from XCode?

Either that, or react-native run-ios

Oh, that's exactly what I did.

  1. Closed packager
  2. Close simulator
  3. react-native run-ios to start packager and the app.

While waiting for your last reply I went ahead and reinstated [self invalidate]; instead of [container invalidate]; as you suggested in the other issue. This didn't change anything regarding the two bugs.

I reinstalled msand/react-native-svg#ce602c1 again and issued a build from XCode. Got rid of the error message and the app works. But everything behaves as before.

How can I confirm that I really have installed the intended version? Any specific code I could look at to verify?

Can you try running this code in it? At least this should be working:

import React, { Component } from 'react';
import { StyleSheet, View, Button } from 'react-native';
import { Svg, Circle, Text, TSpan } from 'react-native-svg';

export default class App extends Component {
  state = { s: 400 };
  render() {
    const { s } = this.state;
    return (
      <View style={styles.container}>
        <View style={{ borderWidth: 2 }}>
          <Svg width={s} height={s} viewBox="0 0 100 100">
            <Circle
              cx="50"
              cy="50"
              r="40"
              stroke="red"
              fillOpacity="0"
            />
            <Text
              textAnchor="middle"
              fontWeight="bold"
              fontSize="4"
              fill="black"
            >
              <TSpan x="50" y="50">
                HOGWARTS
              </TSpan>
            </Text>
          </Svg>
        </View>
        <Button
          title="test"
          onPress={() => {
            this.setState({ s: s === 400 ? 200 : 400 });
          }}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
});

I guess this just means you're experiencing some other issue than this specific thing.

I'll try this in a couple of days. With a bit of luck I might be able to implement a minimal version of my use-case and recreate the bugs in a separate project. I'll put the project in a separate repo.

@msageryd Great, that would be very helpful in analysing this further 馃憤

I think I might have a fix available for iOS here: https://github.com/react-native-community/react-native-svg/commit/d0a9cbaceb71d29d55d8e0f675d7d1608cb99ad2 could you test it and see that it doesn't break any other expectations for you?

@msand I ran in to what I think is the same problem and was digging around a bit prior to finding your fixes. I tried both of them and from what I can tell the android fix is good, but the iOS fix does not correct the behavior in my case.

I'm using this simple example to test it

class App extends React.Component {
  state = { v: 0 }

  componentDidMount() {
    setInterval(() => { this.setState(state => ({ v: state.v + 1 }))}, 100);
  }

  render() {
    return (
      <View style={{ backgroundColor: 'white', flex: 1 }}>
        <Svg width={300} height={300}>
          <Text x={this.state.v} y={100}>{'test'}</Text>
        </Svg>
      </View>
    )
  }
}

I applied both of your fixes, for android the text moves as expected but for iOS it's not. From what I can tell it seems like the RNSVGText and/or RNSVGTSpan elements are not invalidated when the xPosition property is changed, and thus never reaches the cache invalidation and is not re-rendered. If I also change the text content to something like {'test' + this.state.v} so that the content is changed the position is updated correctly. It seems to be due to it hitting (void)setContent which triggers invalidate. Is this somehow related to the xPosition property change not propagating to the tspan child? I'm happy to test any variation of the fix if that might help.

Edit

The issue appeared on version 6.3.0 for both iOS and Android. On 6.2.2 it works as expected. For android the issue appeared with commit 149f460 but for iOS it seems to be an artifact of multiple commits, I wasn't able to track it down to the exact one.

@jgranstrom @msageryd I have a fix available here: https://github.com/react-native-community/react-native-svg/pull/757/commits/926bb71a34ffd251d611e38fbe0029f4e4ff8075
Can you please test it and see if it fixes your issues?
yarn add react-native-svg@msand/react-native-svg#926bb71
npm i react-native-svg@msand/react-native-svg#926bb71

Went through all properties on all element types and fixed the same issue here: https://github.com/react-native-community/react-native-svg/pull/757/commits/c0f51d81a896d4aee95fac9db6cf3d7180746499
I assume this has been the root to all kinds of staleness issues.

@msand great stuff, I felt something like that was missing there but wasn't sure if missed something else that was going on elsewhere. I tried with c0f51d8 and from what I can tell the issue is now resolved for ios as well, thank you! Can we get this together with the android fix published as a patch to npm, or is there a new version coming up that will include it?

@msand Kiitos!
You're the hero of today.

My two big problems went away.

  • Text is re-rendered even if only x and y are set via setNativeProps
  • Text that are transformed via setNativeProps and a matrix does no longer get "mixed up" axis while moving

@msageryd Oj gl枚mde svara h盲r. Vars氓god, kul att underliggande orsakerna hittades o kunde 氓tg盲rdas 馃槂
@jgranstrom The fixes have been published under the 7th major version.
If anyone of you have experienced issues with the gesture responder system, then I would recommend testing the latest commit from the master branch, as I've fixed several issues there recently and help in testing would be very useful! Now onPress/In/out should work and be called the correct number of times with the correct hit target.

Gesture and press handling has been further improved in the master branch, and the updating of text seems to work correctly. So I'll close this now.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dsherida picture dsherida  路  34Comments

eemebarbe picture eemebarbe  路  34Comments

joaom182 picture joaom182  路  32Comments

prashantchothani picture prashantchothani  路  33Comments

raffaeler picture raffaeler  路  117Comments