10

I am currently creating a React Native app and still am unsure about how to best handle states for my use case.

I use react-native-svg to create a rather complex figure in the app, which is stored in a component, stays constant and is retrieved in a parent component. This parent component is then regularly rotated and translated on the canvas, but the path of the complex figure does not change. Therefore, and because the calculations are rather heavy, I would like to calculate the SVG path of that figure only on the start of the app, but not whenever I rotate or translate the parent component. Currently, my code structure looks like this:

Figure Component

const Figure = (props) => {
    const [path, setPath] = useState({FUNCTION TO CALCULATE THE SVG PATH});

    return(
        <G>
            <Path d={path} />
        </G>
    )
}

Parent Component

const Parent = (props) => {
    return (
        <View>
            <Svg>
                <G transform={ROTATE AND TRANSFORM BASED ON INTERACTION}>
                    <Figure />
                </G>
            </Svg>
        </View>
    )
}

As you can see I am using a state for the path data. My questions now:

  1. Am I ensuring here that this path is only calculated once on start?
  2. Is there a more elegant/better way of doing this (e.g. I am not using the setter function at all right now, so I don't feel like this is the best practice)

EDIT:

The function to calculate the path depends on some props I am passing from the parent component.

JoeBe
  • 1,224
  • 3
  • 13
  • 28
  • Your `{FUNCTION TO CALCULATE THE SVG PATH}` will be evaluated **every** time a component re-renders. – zerkms Feb 19 '20 at 01:39
  • @zerkms should I then `useEffect(() => {FUNCTION TO CALCULATE THE SVG PATH}, [])` with emtpy attributes to update the state? – JoeBe Feb 19 '20 at 01:41
  • 1
    But then you don't have a result. Why cannot you call that function outside the component at all? – zerkms Feb 19 '20 at 01:43
  • I see... But what could be a strategy then? – JoeBe Feb 19 '20 at 01:45
  • @zerkms Some parts of the calculations depend on `props` of the parent – JoeBe Feb 19 '20 at 01:47
  • Calculating it in a useEffect, with an empty array as the second arg, will run the calculation once. I think that should be enough. If you're still having problems, consider a memorization scheme for it – Andrew Feb 19 '20 at 01:52
  • @Andrew how will they extract the result out of it though (to pass it to the state) – zerkms Feb 19 '20 at 01:54
  • @Andrew Do you mean `setPath` in the `useEffect`? Then I don't have a result as mentioned by @zerkms. – JoeBe Feb 19 '20 at 01:54
  • What is the data type of the `prop` that the function is reliant on? Is it a primitive like a string or number? – Andrew Feb 19 '20 at 02:27
  • @Andrew [It is a function](https://github.com/d3/d3-geo) that is then called to calculate the figure. – JoeBe Feb 19 '20 at 02:42

3 Answers3

15

When you pass a value to useState that value is used to initialize the state. It does not get set every time the component rerenders, so in your case the path is only ever set when the component mounts.

Even if the props change, the path state will not.

As your initial state depends on a prop, all you need to do is pull the relevant prop out of your props and pass it to the function that calculates the initial path value:

const Figure = (props) => {
  // get relevant prop
  const { importantProp } = props;

  // path will never change, even when props change
  const [path] = useState(calculatePath(importantProp));

  return(
    <G>
      <Path d={path} />
    </G>
  )
}

However, the calculatePath function still gets evaluated every render, even though the path value is not re-initialized. If calculatePath is an expensive operation, then consider using useMemo instead:

You can ensure that path is updated only when a specific prop changes by adding that prop to the dependency array:

const Figure = (props) => {
  const { importantProp } = props;

  const path = useMemo(() => calculatePath(importantProp), [importantProp]);

  return(
    <G>
      <Path d={path} />
    </G>
  )
}

And in that case, you don't need state at all.

Adding the importantProp to the useMemo dependency array means that every time importantProp changes, React will recalculate your path variable.

Using useMemo will prevent the expensive calculation from being evaluated on every render.

I've created a CodeSandbox example where you can see in the console that the path is only re-calculated when the specified prop important changes. If you change any other props, the path does not get recalculated.

JMadelaine
  • 2,859
  • 1
  • 12
  • 17
  • Even though it depends on a prop from the Parent component, this prop will not change. So if this is the case, then my current setup is the way to go for my purpose? – JoeBe Feb 19 '20 at 02:04
  • If I do it with the `useMemo()` approach, then the `path` constant is `undefined` in another `useEffect` I am using in the `Figure` component, which handles other constants to be re-rendered whenever my `Parent` component rotates/translates. – JoeBe Feb 19 '20 at 02:13
  • I've updated my answer, I missed the `() =>` when using `useMemo`. You have to pass it a function to execute, not execute a function inside of it. – JMadelaine Feb 19 '20 at 02:25
  • I've also added a CodeSandbox example – JMadelaine Feb 19 '20 at 02:35
  • Thanks! I got it working. If you don't mind I would like to know what the difference is with `useMemo((), [])` and `useEffect((), [])`, because right now it seems to do the same – JoeBe Feb 19 '20 at 03:04
  • 2
    `useEffect` is used to call a function when the component renders or some dependencies change. This is where you should perform side effects. `useMemo` is used to prevent an expensive object from being recreated on every render, as it will only be recreated when the dependencies change. For your case, either will work, but `useMemo` is more appropriate as you're trying to persist an object rather than run a side effect. Side effects can be anything, including making calls to set state. – JMadelaine Feb 19 '20 at 04:36
  • @JMadeleine The last minutes I tried to read about the difference between the two but nothing until your little comment here made me finally understand it. Thanks for this! – JoeBe Feb 19 '20 at 04:39
6

An even more elegant option is to use the callback form of useState. If you pass it a function, that function will be called only when the initial state needs to be calculated - that is, on the initial render:

const [path, setPath] = React.useState(heavyCalculation);

const heavyCalculation = () => {
  console.log('doing heavy calculation');
  return 0;
};

const App = () => {
  const [path, setPath] = React.useState(heavyCalculation);
  React.useEffect(() => {
    setInterval(() => {
      setPath(path => path + 1);
    }, 1000);
  }, []);
  return 'Path is: ' + path;
};
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class="react"></div>

As you can see in the snippet above, doing heavy calculation only gets logged once.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
2

You can use the hook useMemo, it returns the value to your const, it receives the function that returns the value and also an array, send an empty one to calculate just once, you can send some dependencies, props or state if needed to recalculate the value when they change.

In your case you can do something like this:

const Figure = (props) => {
    const path = React.useMemo(() => {
      // calculate and return value for path
    }, []);

    return(
        <G>
            <Path d={path} />
        </G>
    )
}

It was created for that porpuse.

Hope it helps ;)!

Eliecer Chicott
  • 541
  • 4
  • 7
  • `{FUNCTION TO CALCULATE THE SVG PATH}` --- this expression will be evaluated every time. You probably meant `() => {FUNCTION TO CALCULATE THE SVG PATH}` – zerkms Feb 19 '20 at 01:56
  • 1
    Also "React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components." – zerkms Feb 19 '20 at 01:57
  • Do you by any chance know how useMemo decides whether or not it should do the recalculation? There's no way it's a silver bullet to all memoization issues – Andrew Feb 19 '20 at 02:01
  • If I do it like this, then the `path` constant is `undefined` in another `useEffect` I am using in the `Figure` component. – JoeBe Feb 19 '20 at 02:01
  • @JoeBe I think the useState approach is better than the useEffect one. But you could set the state as null the first round, in the useEffect with empty array calculate and set the value (get to use the setter) and then on the other useEffect listen to path as dependency in there check wether it is different than null. – Eliecer Chicott Feb 19 '20 at 02:08