React JS - can't call setState on a component that is not yet mounted

Viewed 214

I have my Application in React JS. I am fetching data from an API at http://localhost:8080/api/list:

Here is the data that I am getting from the REST API in the Google Chrome console:

0:{
    id: 4
    supplierFirstname: "Tom"
    supplierLastName: "ABC"
    supplierTitle: "TomTheSupplier"
    accountNumber: 1122234444
    address: "111 ADrive, 1234 ST."
    companyName: "TheTom Company & Associates"
    email: "tomtomjayjay@email.com"
    hourlyRate: 29
    phoneNumber: 123456789
    otherPhoneNumber: 1023456789
    paymentTerms: "Credit"
    notes: "Some Supplier"
    createdAt: null
    typeOfGoods: "Supplies"
    website: "www.abc_123.com"
    products: [{…}]
    components: 
        [
            0: {id: 5, name: "AComponent", unit: null, quantity: 0, componentCost: 0, …}
        ]
    
}

Here is my React code:

class SupplierData extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      supplier: [
        {
          id: 0,
          supplierTitle: "Supplier Title",
          supplierFirstName: "First Name",
          supplierLastName: "Last Name",
          companyName: "Company Name",
          phoneNumber: "Phone Number",
          otherPhoneNumber: "Phone Number (Other)",
          accountNumber: "Account Number",
          email: "Email",
          address: "Address",
          website: "Website",
          hourlyRate: "Hourly Rate",
          typeOfGoods: "Type Of Goods",
          paymentTerms: "Payment Terms",
          createdAt: "Created At",
          notes: "Notes",
          products: "Products",
          components: "Components",
        },
      ],
      errorMessage: [],
    };
    this.ListAllSuppliers = this.ListAllSuppliers.bind(this);
  }

  ListAllSuppliers = async () => {
    return await axios
      .get(`http://localhost:8080/api/suppliers/supplier/list`)
      .then((response) => {
        let apiResults = response.data;
        console.log(apiResults);
        this.setState({ supplier: apiResults });    <--- The error happens here.
      })
      .catch((error) => {
        this.setState({ errorMessage: this.state.errorMessage.push(error) });
      });
  };

  componentDidMount() {
    this.ListAllSuppliers();
  }
}

export default SupplierData;

The problem that I am facing is in the React State. I am getting the following error:

Warning: Can't call setState on a component that is not yet mounted. This is a no-op, but it might 
         indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = 
         {};` class property with the desired state in the SupplierData component.

Question: I want to set the state.

What is a possible fix the above error?

1 Answers

The code itself is mostly fine, though you're mixing calls to .then and .catch with async/await which is not best practice, and you're using bind unnecessarily on an arrow function.

It sounds like your component is getting unmounted after you've started your axios call and before it returns, which results in your calling setState on a component that isn't mounted. (Although the wording of the error is slightly different from the one I've seen.)

To avoid that, you can cancel the axios call from componentWillUnmount by passing a cancelToken in the options, and using the cancel method on its source from componentWillUnmount. (The built-in fetch offers the same functionality via AbortController.) (If you were doing something that didn't support cancellation, as a fallback, you could set this.unmounted in componentWillUnmount and then check that and not call setState if you see that this.unmounted is true.)

There are a few other issues with the code; here's an updated version with the cancel token mentioned above and other issues addressed inline (see comments):

class SupplierData extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            supplier: [
                {
                    id: 0,
                    supplierTitle: "Supplier Title",
                    supplierFirstName: "First Name",
                    supplierLastName: "Last Name",
                    companyName: "Company Name",
                    phoneNumber: "Phone Number",
                    otherPhoneNumber: "Phone Number (Other)",
                    accountNumber: "Account Number",
                    email: "Email",
                    address: "Address",
                    website: "Website",
                    hourlyRate: "Hourly Rate",
                    typeOfGoods: "Type Of Goods",
                    paymentTerms: "Payment Terms",
                    createdAt: "Created At",
                    notes: "Notes",
                    products: "Products",
                    components: "Components"
                },
            ],
            errorMessage: [],
        };
        // No need to bind `listAllSuppliers`, you don't use it as a callback

        // A single cancel source that can cancel multiple operations
        this.cancelSource = axios.CancelToken.source();
    }

    // Use an async method
    // Note the initial lower case `l` (not `L`), which is standard in JavaScript
    async listAllSuppliers() {
        // In an `async` function, you use `try`/`catch` and `await`, not
        // `.then` / `.catch` methods
        try {
            const response = await axios.get(
                `http://localhost:8080/api/suppliers/supplier/list`,
                {cancelToken: this.cancelSource.token}
            );
            const supplier = response.data;
            console.log(supplier)
            this.setState({supplier});
        } catch (error) {
            // Updating state based on existing state requires the callback form
            // of `setState` , and you must never directly modify state objects
            this.setState(({errorMessage}) => ({
                errorMessage: [...errorMessage, error]
            }));
        }
    }

    componentDidMount() {
        this.listAllSuppliers();
    }

    componentWillUnmount() {
        // Cancel any outstanding axios calls
        this.cancelSource.cancel("Component unmounted");
    }

    // Presumably you do have a `render` method, you'd get a different React error if you didn't
    render() {
        // ...
    }
}

export default SupplierData;
Related