Tips for Migrating Angular to React

Tips for Migrating Angular to React

There are several non-obvious nuances to React, especially when used with Formik, that without awareness can cause headaches during testing and debugging. Listed below are need-to-knows gathered during my Margin Edge team's code migration from Angular to React, things that will save you a LOT of time if you are new to the library and struggling to figure out why this or that thing is not working as expected:


Getting React to Recognize State Changes in useEffect()

Especially when using Formik, it can sometimes be difficult to get the useEffect() functions to be called as expected when state changes are made. Leaving Formik aside for a second, here’s an example of how you might think to write a useEffect() to be called on array changes

    ...

    useEffect(() => {
      doSomethingWithArray(myArray);
    }, [myArray]);

    ...

The issue is, the above will only be called if the myArray is actually assigned to a new value. If any of its elements are changed, this will not be called at all. There are two ways I’ve found to get around this. One is:

    ...

    useEffect(() => {
      const tempArray = [...myArray];
      doSomethingWithArray(tempArray);
    }, [myArray]);

    ...

and another is:

    ...

    useEffect(() => {
      doSomethingWithArray(myArray);
    }, [JSON.stringify(myArray)];

    ...

but in testing, especially with densely nested objects, I have seen rare cases when the first solution still does not recognize array element changes. The second solution using JSON.stringify(), however, appears to always recognize any and all changes to the array that might occur. But I suspect it is less performant, so if in testing you see that the first solution works in all cases, I would stick to that one.

The next state-change recognition issue I see happens when using Formik. Let’s say you have a simple React setup as follows:

    ...

    const { values, setFieldValue } = useFormik({
        initialValues: initFormData(values)
    });

    <InputField
      value=''
      onChange={(e) => {
          values.myNestedObject.abc.xyz = e.target.value;
      }}
    />

    useEffect(() => {
        if(values.myNestedObject?.abc?.xyz) {
            doSomething(values.myNestedObject.abc.xyz);
        }
    }, [values.myNestedObject?.abc?.xyz]);

    ...

You can see above we are using Formik to supervise a number of aspects of our values object. On line 9, when the user adds text to the InputField, we want to update values.myNestedObject.abc.xyz to the user’s input, which we then want to trigger the useEffect on line 13. While the above code will succeed in changing the myPropery value, it will not call the useEffect() on line 13 because Formik needs to recognize that there was a state change to the values object, and in order to do that, we need to use Formik’s setFieldValue:

    const { values, setFieldValue } = useFormik({
        initialValues: initFormData(values)
    });

    <InputField
      onChange={(e) => {
          setFieldValue('myNestedObject', 
              { abc: { xyz: e.target.value } }
          );
      }}
    />

    useEffect(() => {
        if(values.myNestedObject?.abc?.xyz) {
            doSomething(values.myNestedObject.abc.xyz);
        }
    }, [values.myNestedObject?.abc?.xyz]);

This will successfully trigger the useEffect() on line 17. Note that values does not need to be referenced at the start of setFieldValue because values is a special name when using Formik, and Formik already knows that when you are using setFieldValue it needs to be acting upon the values object. But there is still an issue. We are not preserving the original values in values.myNestedObject, and creating a whole new object with only the xyz property set. This is where the Lodash assign function comes in handy:

    const { values, setFieldValue } = useFormik({
        initialValues: initFormData(values)
    });

    <InputField
      onChange={(e) => {
          setFieldValue('myNestedObject',
            assign({}, values.myNestedObject, {
                abc: 
                    assign(values.myNestedObject.abc, {
                        xyz: e.target.value
                    })
                }
            )
          );
      }}
    />

    useEffect(() => {
        if(values.myNestedObject?.abc?.xyz) {
            doSomething(values.myNestedObject.abc.xyz);
        }
    }, [values.myNestedObject?.abc?.xyz]);

The above solution will preserve all original values in the values object, and only change the myProperty value, and the useEffect will be successfully triggered. You can probably tell using the assign is a little ugly. But if you have nested React components in a scenario where you want a component in, say, the third component layer to trigger a useEffect in the first layer, you may need to do something like this. (By “layer” I mean, if you have React component X that holds components Y and Z, and component Z is only visible when some condition is met in Y, and component Y is only visible when some condition is met in X)

Note that you will not be able to use setFieldValue in a non-component utilities class in a pretty way, so if you are using a utilities class, it might be best to have the utility class return the value(s) you have changed, then use setFieldValue with those new values to trigger the changes in the React component that is calling the utilities class. Something like this:

    const { values, setFieldValue } = useFormik({
        initialValues: initFormData(values)
    });

    <InputField
      onChange={(e) => {
          const changedObject = myUtils(values.myNestedObject);
          setFieldValue('myNestedObject',
            assign({}, values.myNestedObject, cloneDeep(changedObject)
          );
      }}
    />

    useEffect(() => {
        if(values.myNestedObject?.abc?.xyz) {
            doSomething(values.myNestedObject.abc.xyz);
        }
    }, [values.myNestedObject?.abc?.xyz]);

Unable to Set data-testid In React Components

You’ll find that setting a data-testid value in your React components can be extremely useful during testing. It allows you to very easily identify and toy with your components during testing using fireEvent. Let’s say you’re using the Input component:

   <Input
      data-testid='myAwesomeField'
      value=''
      onChange={(e) => {
          values.myNestedObject.abc.xyz = e.target.value;
      }}
    />

The idea here is so that in your tests, you will easily be able to play with this component using fireEvent in your tests like so:

fireEvent.change(
    getByTestId('myAwesomeField'), { target: {value : 'newVal'} });

expect(getByTestId('myAwesomeField')).toHaveValue('newVal');

The issue is that the Input component, like many other components, doesn’t actually accept a data-testid value. It does, however, accept inputProps:

   <Input
      inputProps={{ 'data-testid': 'myAwesomeField' }}
      value=''
      onChange={(e) => {
          values.myNestedObject.abc.xyz = e.target.value;
      }}
    />

The above change to using inputProps will allow you to use getByTestId in your React tests. There are still some components you’ll come across that take neither a data-testid or inputProps, and in these cases you will likely need to set a placeholder value and use getByPlaceHolderValue in your tests, or some other value to get your jest tests to grab the wanted component.


Generic Errors in Jest Tests When Using fireEvent

Let's say you have a React test like so:

test('my cool sweet test', async () => {
    ...

    let renderer: RenderResult = undefined;
    await act(async () => {
        renderer = renderWithRouter(
            <UserContext.Provider>
                </MyContext.Provider>
            </UserContext.Provider>
        );
    });

    fireEvent.change(renderer.getByPlaceholderText('Name'), 
            { target: { value: 'newVal' } });
    fireEvent.blur(renderer.getByPlaceholderText('Name'));
});

and in your React code, you accidentally misnamed your Name input field to Nyme and thus your test does not recognize the Nyme placeholderText and fails. With the above test setup, the test’s console output is likely going to give you a generic “An error occurred, make sure to wrap your fireEvent calls in an act()…”. The message is simple enough, so you would think to move your fireEvent calls into the act() block so your error messages are clear and actually helpful:

test('my cool sweet test', async () => {
    ...

    let renderer: RenderResult = undefined;
    await act(async () => {
        renderer = renderWithRouter(
            <UserContext.Provider>
                </MyContext.Provider>
            </UserContext.Provider>
        );

        fireEvent.change(renderer.getByPlaceholderText('Name'), 
                { target: { value: 'newVal' } });
        fireEvent.blur(renderer.getByPlaceholderText('Name'));
    });
});

But the above is unfortunately still going to give you the same generic “Make sure to wrap in an act()” error message. The way around this I’ve found is to wrap each failing fireEvent call with an individual act(), and the error messages will tell you exactly where the code is failing and why:

test('my cool sweet test', async () => {
    ...

    let renderer = undefined;
    await act(async () => {
        renderer = renderWithRouter(
            <UserContext.Provider>
                </MyContext.Provider>
            </UserContext.Provider>
        );

        await act(async () => {
            fireEvent.change(renderer.getByPlaceholderText('Name'), 
                { target: { value: 'newVal' } });
        });

        await act(async () => {
            fireEvent.blur(renderer.getByPlaceholderText('Name'));        
        });
    });
});

The above changes will give you clear and exact error messaging output as expected. After you have the test issues sorted out and passing, you can remove the await act changes and revert the code back to the original example of this section, without the act wrappers. The only case I have seen where you might need to keep the await act() wrappers is when toggling a button that triggers a non-trivial action, because the fireEvent button clicks happen so quickly that the trigger/onChange behavior has not completed by the time the second button click happens, so the await act pieces are still needed.


This is a working document, so expect further tips to come. Thanks for reading!