0

This seems to be a common issue, but I've tried several configurations and am consistently getting the error message Warning: Can't call setState on a component that is not yet mounted. I have used setState() in similar ways in other components of my app without issue, so I'm not sure why my App component thinks I'm trying to call setState() in the constructor when it is very clearly outside of it.

This is my attempt to use the componentDidMount() lifecycle method. There is clearly something I am fully missing about this, but I can't glean much reading the documentation for setState(). Especially considering that I am using this exact, same syntax in other parts of my app without issue. At any rate, here's what I'm working with:

import react from 'react';

class App extends react.Component {
  constructor() {
    super();
    this.state = {
      tasks: [
        {
          content:"walk dog",
          date:"7/17/21",
          priority:"high"
        },
        {
          content:"take out trash",
          date:"7/17/21",
          priority:"low"
        },
      ],
    };
  }
  componentDidMount() {
    this.addTask = this.addTask.bind(this);
  }
  addTask(content, date, priority) {
    let taskUpdate = this.state.tasks;
    let task = {
      content: content,
      date: date,
      priority: priority
    };
    taskUpdate.push(task);
    this.setState({
      tasks: taskUpdate
    });
  }

  render() {
    return (
      <>
        <Header />
        <Sidebar taskList={this.state.tasks} groupList={this.state.groups}>
        </Sidebar>
      </>
    )
  }
}

export { App };

The gist of this is to update the state of my App component so that I can pass as props and re-render the list of tasks within the Sidebar component. Apologies in advance since this seems to have been asked a million times, but I am simply confounded.

EDIT

For clarification, the addTask method is called within a form that is rendered as part of an "add this task to the list of tasks" modal, e.g.:

const AddModal = ({ closeBtn, show, children }) => {
    class Form extends Component {
        constructor(props) {
            super(props);
            this.state = {
                content: '',
                date: '',
                highPriority: ''
            };
            this.handleContent = this.handleContent.bind(this);
            this.handleDate = this.handleDate.bind(this);
            this.handlePriority = this.handlePriority.bind(this);
            this.handleSubmit = this.handleSubmit.bind(this);
        }
        handleContent(event) {
            this.setState({content: event.target.value});
        }
        handleDate(event) {
            this.setState({date: event.target.value});
        }
        handlePriority(event) {
            this.setState({highPriority: event.target.checked});
        }
        handleSubmit(event) {
            event.preventDefault();
            console.log("content: " + this.state.content + "\n" + 
                        "date: " + this.state.date + "\n" +
                        "high priority: " + this.state.highPriority);
            new App().addTask(this.state.content, this.state.date, this.state.highPriority);
            closeBtn();
            
        }
        render() {
            return (
                <form onSubmit={this.handleSubmit}>
                    <input 
                    type="text"
                    value={this.state.content} 
                    onChange={this.handleContent}
                    placeholder="add a task (max 30 chars)"
                    maxLength="30"></input>
    
                    <input 
                    type="date"
                    onChange={this.handleDate}></input>
    
                    <div className="checkbox-container">
                        <input
                        type="checkbox"
                        name="priority-checkbox"
                        onChange={this.handlePriority}></input>
                        <label
                        htmlFor="priority-checkbox">
                            high priority?
                        </label>
                        <button
                            type="submit"
                            id="add-task-submit">
                            +
                        </button>
                    </div>
    
                    
                </form>
            );
        }
    };

    const modalClassName = show ? "add-task-modal display-block" : "add-task-modal display-none";
    return ( 
        <div className={modalClassName}>
            <div className="add-task-modal-content">
                {children}
                <button type="button" id="close-btn" onClick={closeBtn}>
                    x
                </button>

                <Form></Form>
            </div>
        </div>
    );
};

The modal containing this form is rendered in a file that simply displays a list of tasks. The addTask() method is successful in passing form data to 'App.js' as I can change the state directly, e.g. this.state.tasks = [...], but from what I have read this is extremely not best practice, and I need to use setState() instead.

EDIT 2

So what started as me stumbling over a simple issue of altering state turned into a succinct and useful overview of react as a framework. Big thanks to user Robin Zigmond for the write-up and to Alexander Staroselsky for expounding even further with the suggestion of using the redux library. Your input helped immensely, and the components of my app are now correctly communicating with one another.

Suffice it to say my initial bug was not really an issue with setState, this has been instrumental in helping me understand react on a broader level. Thanks again to everyone who contributed.

hbarnett91
  • 43
  • 5
  • 1
    Where is addTask being called exactly? – Alexander Staroselsky Jul 25 '21 at 17:52
  • Is this the full component? I don't see anywhere `addTask` is called, and that's the only method that calls `setState`. I do note that `this.addTask = this.addTask.bind(this);` should be in the constructor, not `componentDidMount`, and that might well cause you issues - but it shouldn't cause this particular console warning as far as I can tell. – Robin Zigmond Jul 25 '21 at 17:53
  • @AlexanderStaroselsky `addTask()` is called when submitting a form in a modal window. This function works fine if I update the state directly, e.g. `this.state.tasks = ...`, but I want to use `setState()` so that I can actually render the change to the list. Or at least that's my understanding of `setState()`. – hbarnett91 Jul 25 '21 at 17:58
  • @RobinZigmond I have also attempted to bind the function in the constructor, but I get the same error. `addTask` is called in an "add this task to the list of tasks" modal elsewhere in the app. As mentioned in another comment, the function works perfectly well when I update the state directly, e.g. `this.state.tasks = ...`, but I realize that is not best practice, and I would like to actually utilize the `setState()` method. – hbarnett91 Jul 25 '21 at 18:01
  • Have you changed something in your webpack config?? Maybe this issue is caused by some configuration. Or some third party package maybe causing it. – Zaeem Khaliq Jul 25 '21 at 18:16
  • @hbarnett91 so it sounds like you are indeed calling this somewhere you don't show us in the original question. Could you add that? The warning you are getting indicates that you are calling it before this component has mounted, it's hard to suggest how to fix it before seeing how you are actually calling it. – Robin Zigmond Jul 25 '21 at 18:24
  • @RobinZigmond I have updated my OP to include the modal in which `addTask` is called. Perhaps I am thinking about this totally wrong. Is `App` not mounted by the time everything is rendered to the extent that I could even access the button that calls `addTask`? Does that make sense? – hbarnett91 Jul 25 '21 at 18:30
  • Thank you, that does indeed show exactly what you are doing wrong - which seems to be based on a large misunderstanding about how React works. I will type up my thoughts into an answer! – Robin Zigmond Jul 25 '21 at 18:49

3 Answers3

0

You shouldn't mutate array in place, when you use push reference to array doesn't change. But not sure if this related to your warning.

  addTask(content, date, priority) {
    this.setState({
      tasks: this.state.tasks.concat({
        content,
        date,
        priority
      })
    });
  }

Why you calling addTask with new? You not supposed to do this.

new App().addTask(this.state.content, this.state.date, this.state.highPriority);

Can use context or redux for pass value and update function to modal.

  • This is much cleaner, thank you. Unfortunately I am still receiving the same error, so the search continues. – hbarnett91 Jul 25 '21 at 18:07
  • Need more info, how you pass addTask cb and how you use it, because don't see anything wrong except mutating array. – Aleksandr Smyshliaev Jul 25 '21 at 18:13
  • I have updated my post to include the modal in which `addTask` is called. – hbarnett91 Jul 25 '21 at 18:26
  • Possible that you call `addTask` with `new`, shouldn't do like this. – Aleksandr Smyshliaev Jul 25 '21 at 18:29
  • `new App().addTask()` is my ad hoc solution to pass form data to be handled by the main logic of my application. It didn't seem right when I was typing it out, but it does successfully return data from my form/modal. – hbarnett91 Jul 25 '21 at 18:36
  • Try to comment it out and see if warning disappear, sure it's not right. – Aleksandr Smyshliaev Jul 25 '21 at 18:37
  • This does remove the error since the method is no longer being called. What led to my initial 'solution' was attempting to export a function that updates the state of `App` from another part of the application. I'm not sure how to get `addTask` to work the way I need it to. – hbarnett91 Jul 25 '21 at 18:42
  • You can use `context` to pass `addTask` from top parent component to modal. – Aleksandr Smyshliaev Jul 25 '21 at 18:44
  • I suspect this will work perfectly once I am able to get back to my computer. I think part of my problem from the start was thinking too much in terms of imports and exports instead of using the sort of cascading effect from parent to child components with regard to their methods. I'll update as soon as I can, thank you so much for your help this far. – hbarnett91 Jul 25 '21 at 19:02
  • Both yours and user Robin Zigmond's advice helped me fix my problem, which was obviously a misunderstanding on my part in handling props from parent to child in react. Thank you so much for your feedback. – hbarnett91 Jul 25 '21 at 19:59
0

You can replace your addTask function with this:

addTask(content, date, priority) {
    
    let task = {
      content: content,
      date: date,
      priority: priority
    };
    
    this.setState({
      tasks: [...this.state.tasks, task]
    });
  }
Ingenious_Hans
  • 724
  • 5
  • 16
0

The problem can be seen here in your AddModal component:

new App().addTask(this.state.content, this.state.date, this.state.highPriority);

Manually making a new instance of one of your components - as you do here with new App() - is not something that you should ever need to do in React. The framework itself handles the lifecycle of all components - including constructing them.

The warning itself is revealing in this context: "Can't call setState on a component that is not yet mounted". "mounted" is a concept in React which refers simply to whether a component instance is yet rendered as part of your user interface - in other words, whether it is on the page or not. React is a library/framework whose sole purpose is to build a user interface - and each component should be something where you could point a non-technical user to part of your page/app and say "that is the part this component controls". It doesn't make any sense to have a component instance that isn't actually rendered (or "mounted" to use the React term) on the page.

Technically, React will not even call the constructor of your class until it is ready to "mount" the component - so after the constructor is called, React will call your component's render method and then (if it has one) its componentDidMount method. (See React component lifecycle diagram here.) React handles all this itself when you include a component in your JSX inside render - so you don't have to worry about it. But by manually instantiating new App() you are precisely constructing a component instance without mounting it, which React is not designed to deal with, and therefore it justifiably complains.

What you appear to be trying to do doesn't make sense on a deeper level - because you are trying to update component state, but the component shouldn't have any state when it's not yet mounted. Again, state should be something that corresponds to something in your UI, even on a slightly abstract level - it might be what the user has currently typed into an input field, or something a bit more abstract like a flag to say that some action has or hasn't been done, or a count of the number of times a particular action has been done - or whatever else you feel like. But it corresponds to something, and that something is intimately linked to what is going on inside your component on the page. If it's not on the page - then what is your state actually for?

To return to what you should actually do: I assume your App is the root component of your application, and that AddModal appears somewhere inside it. If it were a direct child, this would be simple, because your App is always rendered when AddModal is and you can simply pass the addTask method down as a prop. In other words, somewhere inside render in App, you would have:

<AddModal addTask={ this.addTask } />

and then inside AddModal call this as:

this.props.addTask(/*whatever arguments you need*/);

which would update the state of the parent component to what you need. (Which here seems to be updating the data inside the Sidebar component.)

If, as seems likely, the AddModal is actually several levels down, you have to pass addTask down several levels. Ie. inside render of App you would have

<SomeChildComponent addTask={ this.addTask } />

then inside SomeChildComponent, say

<SomeOtherChild addTask={ this.props.addTask } />

and so on down whatever levels you need, until you finally reach

<AddModal addTask={ this.props.addTask } />

If you have more than 1 or 2 levels of this and don't need the addTask on the intermediate levels, this certainly gets tedious and brittle - so you may want to look at other solutions like context, or Redux. But this is how you do things in "basic React", and I think you'd do well to understand this before looking too hard at other patterns. (I'm a big believer in needing to understand what problems libraries like Redux are solving before just diving into using them because "everyone else does".)

I'd also recommend reading this for an understanding of how to deal with situations where one component needs to update the state of another which isn't a direct child or parent - which seems to be what you're trying to do.

Apologies if I've been rambling, but I hope this gives you a better handle on how React works and how to deal with state, and why you shouldn't be manually constructing your own components.

Robin Zigmond
  • 17,805
  • 2
  • 23
  • 34