James
James

Reputation: 461

Why is setState being reset?

I'm working on a community site where users can share and comment on websites (here it is)[https://beta.getkelvin.com/]. Websites can be filtered by category, but when a user goes into a specific page then backs out, the filter is removed.

Here's the process in steps:

Instead of reverting back to 0, I want the value to still apply so the filter remains on the same category that the user selected before.

The problem seems to be that getInitialState is resetting the value of this.state.topicSelected back to 0 (as it's written in the code). When I try to put a dynamic value in 0's place, I get an undefined error.

Here's the getInitialState code:

        var SitesArea = React.createClass({
            getInitialState: function () {
                return {
                    sortSelected: "most-recent",
                    topicSelected: 0 // need to overwrite with value of this.state.topicSelected when user selects filter
// I removed other attributes to clean it up for your sake
                }
            }

On here's the event:

            onTopicClick: function (e) {
                e.preventDefault();
                this.setState({topicSelected: Number(e.target.value)});
                if (Number(e.target.value) == 0) {
                    if (this.state.sortSelected == 'highest-score') {
                        this.setState({sites: []}, function () {
                            this.setState({sites: this.state.score});
                        });
                    } else if (this.state.sortSelected == 'most-remarked') {
                        this.setState({sites: []}, function () {
                            this.setState({sites: this.state.remarkable});
                        });
                    } else if (this.state.sortSelected == 'most-visited') {
                        this.setState({sites: []}, function () {
                            this.setState({sites: this.state.visited});
                        });
                    } else if (this.state.sortSelected == 'most-recent') {
                        this.setState({sites: []}, function () {
                            this.setState({sites: this.state.recent});
                        });
                    }
                } else {
                    this.getSites(this.state.sortSelected, Number(e.target.value));
                    this.setState({sites: []}, function () {
                        this.setState({sites: this.state.filter_sites});
                    });
                }

And lastly, the dropdown menu:

<select
  value={this.state.topicSelected}
  onChange={this.onTopicClick}
  className="sort"
  data-element>
    {
// Add this option in the .then() when populating siteCategories()
 [<option key='0'value='0'>Topics</option>].concat(
this.state.topics.map(function (topic) {
       return (<option
       key={topic.id}
       value={topic.id}>{topic.name}</option>);
  }))
}

How do I get it so that this.state.topicSelected doesn't get reset when a user goes back to the main page?

Upvotes: 0

Views: 1243

Answers (1)

bigh_29
bigh_29

Reputation: 2633

I think your main page is getting unmounted (destroyed) when the user navigates from the main page to the summary page. React creates a brand new instance of the main page component when they return. That reconstruction initializes the selected topic back to 0.

Here is a codepen that shows what might be happening. Foo == your main page, Bar == summary page. Click Increment topic a couple times, then navigate from Foo to Bar and back again. Console logs show that the Foo component gets unmounted when you navigate away, and reconstructed on return.

Note You seem to be using an older version of react, as evidenced by the presence of getInitialState and React.createClass. My pen follows the more modern approach of initializing state in the class constructor.

To solve the problem, you have to save that state outside the main component in something that isn't getting deleted and re-created as they navigate. Here are some choices for doing that

  • Expose an onTopicSelected event from your main page. The parent of the main page would pass in a function handler as a prop to hook that event. The handler should save the selected topic in the component state of the parent. This is kind of messy solution because the parent usually should not know or care about the internal state of its children
  • Stuff the selected topic into something that isn't react, like window.props. This is an ugly idea as well.
  • Learn about redux and plug it into your app.

Redux is the cleanest way to store this state, but it would require a bit of learning. I have implemented the first solution in this codepen if you want to see what it would look like.

The original codepen showing the problem is pasted below as a snippet. Switch to Full page mode if you try to run it here.

//jshint esnext:true

const Header = (props) => {
    const {selectedPage, onNavigationChange} = props;
    const disableFoo = selectedPage == 'foo'
    const disableBar = selectedPage == 'bar';

    return (
        <div>
            <h1>Header Component : {selectedPage}</h1>
            <button disabled={disableFoo} onClick={() => onNavigationChange('foo')}>Foo page</button>
            <button disabled={disableBar} onClick={() => onNavigationChange('bar')}>Bar page</button>
            <hr/>
        </div>
    );
};

class Foo extends React.Component {
    constructor(props) {
        console.log('Foo constructor');
        super(props);
        this.state = {
            topicSelected: 0
        };
        this.incrementTopic = this.incrementTopic.bind(this);
    }


    incrementTopic() {
        const {topicSelected} = this.state
        const newTopic = topicSelected + 1
        console.log(`incrementing topic: old=${topicSelected} new=${newTopic}`)
        this.setState({
            topicSelected: newTopic
        })
    }

    render() {
        console.log('Foo::render');
        return (<div>
            <h2>The Foo content page : topicSelected={this.state.topicSelected}</h2>
                <button onClick={this.incrementTopic}>Increment topic</button>
            </div>
        );
    }

    componentWillMount() {
        console.log('Foo::componentWillMount');
    }

    componentDidMount() {
        console.log('Foo::componentDidMount');
    }

    componentWillUnmount() {
        console.log('Foo::componentWillUnmount');
    }

    componentWillUpdate() {
        console.log('Foo::componentWillUpdate');
    }

    componentDidUpdate() {
        console.log('Foo::componentDidUpdate');
    }
}


const Bar = (props) => {
    console.log('Bar::render');
    return <h2>The Bar content page</h2>
}

const Body = (props) => {
    console.log('Body::render');

    const {selectedPage} = props;

    if (selectedPage == 'foo') {
        return <Foo/>;
    } else if (selectedPage == 'bar') {
        return <Bar/>
    } 
};


class Application extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            selectedPage: 'foo'
        };
    }

    render() {
        console.log('Application::render');
        const {selectedPage} = this.state;
        const navigationChange = (nextPage) => {
            this.setState({
                selectedPage: nextPage
            })
        }
        return (
            <div>
                <Header selectedPage={selectedPage} onNavigationChange={navigationChange}/>
                <Body selectedPage={selectedPage}/>
            </div>
        );
    }
}


ReactDOM.render(<Application/>,
    document.getElementById('main')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="main"></div>

Upvotes: 2

Related Questions