Nemus
Nemus

Reputation: 1482

React-Native: cannot update a component while rendering a different component

I've got this simple component Login:

function Login() {
  const [isFormValidState, setIsFormValidState] = React.useState(false);
  const [credentialState, setCredentialState] = React.useState();

  function getFormErrors(errors: any, dirty: boolean) {
    setIsFormValidState(!Object.keys(errors).length && dirty);
  }

  function getFormValues(values: any) {
    setCredentialState(values);
  }

  function doAction() {
    //credentialState rest call...
  }

  return (
    <View>
      <Text>Login</Text>
      <UserCredentialForm getFormValues={getFormValues} getFormErrors={getFormErrors}/>
      <Button title='Entra' disabled={!isFormValidState} onPress={doAction}/>
    </View>
  );
}

Which calls UserCredentialForm:

export default function UserCredentialForm({ getFormValues, getFormErrors }) {
[...]
  return (
    <Formik innerRef={formRef} validationSchema={formSchema} initialValues={state.form} onSubmit={() => { }}>
      {({ handleChange, values, touched, errors, dirty }) => {
        getFormValues(values);
        getFormErrors(errors, dirty);
        return <React.Fragment>
          // <TextInput/>....              
        </React.Fragment>
      }}
    </Formik>
  );

[...]
}

While navigating in my app I've got this error:

react native cannot update a component Login while rendering a different component Formik.

Then it points me to the error in the setCredentialState inside getFormValues handler in Login component. I've resolved this using a ref instead of a state, but the problem itself is unsolved to me.

What if I need to update my parent component view after a child event?

Upvotes: 5

Views: 2545

Answers (5)

Collins Olix
Collins Olix

Reputation: 91

I got a similar error while using reactNativeNavigation,

I had a TouchbleOpacity with an onPress event handler that navigates to a different screen

however I had it written like this ...

    <TouchableOpacity onPress={navigation.navigate("OtherScreen")}>...

which obviously calls the function to execute even though it hadn't been Touched yet

So I corrected it this way...

    <TouchableOpacity onPress={()=> navigation.navigate("OtherScreen")}>...

which doesn't call the function.

This answer is for people who may experience similar errors, even though the code has nothing to do with settingState

Upvotes: 0

Khurram W. Malik
Khurram W. Malik

Reputation: 2905

You are calling getFormValues and getFormErrors inside a callback provided by Formik, that means you cannot wrap them inside an effect hook to suppress this warning or it will violate rules of hooks.

I faced the same issue in React JS and got rid of it by using the following: approach.

I used useFormik hook as an alternate.

and afterwards I refactored Formik form into a new component from where I made state changes to parent component. This way I neither violated rules of hooks nor got this warning.

Also in the that newly refactored component you might need useFormikContext and useField

Upvotes: 0

Hoang
Hoang

Reputation: 166

The reason for that error is because you call setState inside render(). The call getFormValues(values), which set the state of credentialState is called inside the render.

When the state is set, the Login component get rerendered, thus recreating a new function of getFormValues. As this is used as the prop of UserCredentialForm, it also causes that component to rerender, which causes the render prop inside Formik to calls again, which calls getFormValues causing the state change, causing an infinite loop.

One solution you can try is to add useCallback to the two functions, which prevent them to have new identities after the state changes and consequently change the props, thus creating infinite rerender.

function Login() {
  const [isFormValidState, setIsFormValidState] = React.useState(false);
  const [credentialState, setCredentialState] = React.useState();

  const getFormErrors = useCallback(function getFormErrors(errors: any, dirty: boolean) {
    setIsFormValidState(!Object.keys(errors).length && dirty);
  }, []);

  const getFormValues = useCallback(function getFormValues(values: any) {
    setCredentialState(values);
  }, []);

  function doAction() {
    //credentialState rest call...
  }

  return (
    <View>
      <Text>Login</Text>
      <UserCredentialForm getFormValues={getFormValues} getFormErrors={getFormErrors}/>
      <Button title='Entra' disabled={!isFormValidState} onPress={doAction}/>
    </View>
  );
}

However, there is still an issue and that is the identity of values may not be stable and by setting it to state, it will keep causing rerender. What you want to do is to tell UserCredentialForm not to rerender even when that state changes, and since the state is not used as a prop in UserCredentialForm, you can do that with React.memo.


export default React.memo(function UserCredentialForm({ getFormValues, getFormErrors }) {
[...]
  return (
    <Formik innerRef={formRef} validationSchema={formSchema} initialValues={state.form} onSubmit={() => { }}>
      {({ handleChange, values, touched, errors, dirty }) => {
        getFormValues(values);
        getFormErrors(errors, dirty);
        return <React.Fragment>
          // <TextInput/>....              
        </React.Fragment>
      }}
    </Formik>
  );

[...]
})

Upvotes: 2

Nooruddin Lakhani
Nooruddin Lakhani

Reputation: 6967

Simple example can be like

UserCredentialForm:

 // Formik x React Native example
 import React from 'react';
 import { Button, TextInput, View } from 'react-native';
 import { Formik } from 'formik';
 
 export const MyReactNativeForm = ({onSubmit}) => (
   <Formik
     initialValues={{ email: '' }}
     onSubmit={values => onSubmit(values)}
   >
     {({ handleChange, handleBlur, handleSubmit, values }) => (
       <View>
         <TextInput
           onChangeText={handleChange('email')}
           onBlur={handleBlur('email')}
           value={values.email}
         />
         <Button onPress={handleSubmit} title="Submit" />
       </View>
     )}
   </Formik>
 );

Usage like

function Login() {

  function doAction(values) {
    console.log(values);
    //credentialState rest call...
  }

  return (
    <View>
      ....
      <UserCredentialForm onSubmit={doAction} />
      ....
    </View>
  );
}

Upvotes: -1

btb
btb

Reputation: 180

I think you got an unlimited loop of rendering,

you setState by getFormValues and the Login component re-render make UserCredentialForm re-render too, so it call getFormValues again and again

You can call getFormValues(values) in a useEffect hook after values of formik update

Upvotes: 1

Related Questions