Eric Cai
Eric Cai

Reputation: 83

Why child component's prop is only showing parent component's initial value?

I am building a react single page app that will call API to search based on a user's input, and the user has to log in to be able to use the app.

The hierarchy of the components are:

  1. App.js (Handles login and routing)
  2. <SearchPage /> and <SearchResults />
  3. There are <SearchNavBar /> and <SearchMain /> under <SearchPage /> (<SearchMain /> contains the form and input for users to submit, and after submitting, users should be navigated to the route that <SearchResults /> is at.)

Here is the issue: Since react only allows one direction data flow, I have to pass user's input from <SearchMain /> upwards to App.js, and then save it in the state of App.js, and pass it down as a prop to <SearchResults /> together with the token got from App.js. However, even I have updated the state in App.js, and passed it as a prop to its child component <SearchResults />, while console.logging the props of the child component <SearchResults />, I could only get the initial values of the state, not the updated ones, why??

Code:

App.js:

const myMSALObj = new UserAgentApplication(msalConfig);
class App extends Component {
  constructor(props) {
       super(props)
    this.state = {
      res:'',
      isLoggedIn:false,
      keyword:''
    }        
    this.handleLogOut = this.handleLogOut.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }    
  handleLogOut(){
    myMSALObj.logout();
    this.setState({isLoggedIn:false})
  }   
  handleSubmit (typedValue){
      this.setState({keyword:typedValue},() => console.log(this.state));//here works just fine, showing correct state value
      window.location.replace('/results')
    };

  async componentDidMount(){

    const idToken_response = await myMSALObj.loginPopup(loginRequest);
    const response = await myMSALObj.acquireTokenPopup(loginRequest);

    this.setState({isLoggedIn:true,res:response});
  }
  render(){
    return (
    <Router>
      <div>
        <Switch>
        {this.state.isLoggedIn === true ? 
        <Route exact path="/" render={() => <SearchPage 
                                             parentState = {this.state} 
                                             handleLogOut = {this.handleLogOut}
                                             handleSubmit = {this.handleSubmit} />}/>:
        <Route exact path="/" render={() => (<div>Please wait till you are logged in...</div>)}/>}
        <Route exact path="/results" render={() => <SearchResults 
                                                    parentState = {this.state.keyword} 
                                                    handleLogOut = {this.handleLogOut} />}/>          
        </Switch>
      </div>
    </Router>
    );
  }
}
export default App;

SearchPage:

class SeachPage extends Component{

    constructor (props){
        super (props);
        this.state = {
            // userInfo: this.props,
            accessToken: this.props.parentState.res.accessToken,
            name:this.props.parentState.res.account.name,
            email:this.props.parentState.res.account.userName
        }
        // console.log(this.props)
    }
    render(){
        return (
            <Fragment>
                <SearchNavBar 
                 info={this.state} 
                 handleLogOut = {this.props.handleLogOut}/>
                <SearchMain 
                 accessToken={this.state.accessToken } 
                 handleSubmit = {this.props.handleSubmit}/>
            </Fragment>

        )
    }
}

export default SeachPage;

SearchMain:

class SearchMain extends Component{
    constructor(props){

        super(props);

        this.state = {
            token:this.props.accessToken,
            keyword:'',
            typed:'',
        }

        this.handleChange = this.handleChange.bind(this);
        this.handleSubmitForm = this.handleSubmitForm.bind(this);

        // console.log(this.props)
    }

    handleChange(event) {
        let typed = event.target.value;
        let keyword = event.target.value;
        this.setState({typed: typed,keyword:keyword},() => console.log(this.state));

      };

    handleSubmitForm(event){
        const keyword = this.state.keyword;
        this.props.handleSubmit(keyword);
        event.preventDefault();
    }

    render(){
        return(
            <div className="container" id="searchBox">
            <form onSubmit = {this.handleSubmitForm } className="field is-grouped"> 
                <p className="control is-expanded">
                <input type="text" className="input" value={this.state.keyword} onChange={this.handleChange} autoComplete="off" placeholder="Find an asset" required></input>
                </p>
                <p className="control">
                    <button id="button-in-nav-bar" type="submit" className="button is-primary"><strong>Search</strong></button>
                </p>
            </form>
            </div>
        )
    } 

};

export default SearchMain;

SearchResults

class SearchResults extends Component {

    constructor(props){
        super(props);
        console.log(this.props) //here is the issue!! onlyshowing{res:'',isLoggedIn:false,keyword:''}
    }

    render (){
        return(
            <div>test results</div>
        )
    }
};

export default SearchResults;

Update So briefly, I was able to pass the keyword upwards to the parent component, just seem to be a little late, the screenshots may better describe my struggle: App.js console and if I uncomment the window.location.replace('/results') in the first screenshot, the page will be navigated to '/results', and if I console.log(this.props) in the render function searchResults.js, the props were displayed 4 times, and the last 2 times had partly correct info.enter image description here, still the keyword was missing... I had a feeling that this might bc of the async nature but I really have zero clue on how to fix it.. (side question, why it was desiplayed 4 times?)

Upvotes: 2

Views: 256

Answers (2)

gdh
gdh

Reputation: 13682

First Issue

Using window.replace will reload the page and all your state is initialised back to original values. Hence you will lose keyword state. So you have to wrap your App with Router and use history in handleSubmit.

Second issue

In SearchMain, you are deriving the all the state from props in your state i.e. inside constructor(which gets executed once). Hence, further updates are not available for the SearchMain component. So just use props (don't use state)

Updated App.js

const myMSALObj = new UserAgentApplication(msalConfig);
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      res: {}, //<--- initialise as an empty object(not string)
      isLoggedIn: false,
      keyword: "",
    };
    this.handleLogOut = this.handleLogOut.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  handleLogOut() {
    myMSALObj.logout();
    this.setState({ isLoggedIn: false });
  }
  handleSubmit(typedValue) {
    this.setState({ keyword: typedValue }, () => console.log(this.state)); //here works just fine, showing correct state value
    // window.location.replace("/results");
    this.props.history.replace("/results")
  }

  async componentDidMount() {
    const idToken_response = await myMSALObj.loginPopup(loginRequest);
    const response = await myMSALObj.acquireTokenPopup(loginRequest);

    this.setState({ isLoggedIn: true, res: response });
  }
  render() {
    return (
        <div>
          <Switch>
            {this.state.isLoggedIn === true ? (
              <Route
                exact
                path="/"
                render={() => (
                  <SearchPage
                    parentState={this.state}
                    handleLogOut={this.handleLogOut}
                    handleSubmit={this.handleSubmit}
                  />
                )}
              />
            ) : (
              <Route
                exact
                path="/"
                render={() => <div>Please wait till you are logged in...</div>}
              />
            )}
            <Route
              exact
              path="/results"
              render={() => (
                <SearchResults
                  parentState={this.state.keyword}
                  handleLogOut={this.handleLogOut}
                />
              )}
            />
          </Switch>
        </div>
    );
  }
}
export default (props) => (
  <Router>
    <Route render={(rProps) => <App {...props} {...rProps} />} />
  </Router>
);

Updated SearchMain.js

class SeachPage extends Component{

    constructor (props){
        super (props);
        // this.state = {
        //     // userInfo: this.props,
        //     accessToken: this.props.parentState.res.accessToken,
        //     name:this.props.parentState.res.account.name,
        //     email:this.props.parentState.res.account.userName
        // }
        // console.log(this.props)
    }

    render(){
        return (
            <Fragment>
                <SearchNavBar 
                 info={this.props} // <----- use props (not state)
                 handleLogOut = {this.props.handleLogOut}/>
                <SearchMain 
                 accessToken={this.props.accessToken } // <----- use props (not state)
                 handleSubmit = {this.props.handleSubmit}/>
            </Fragment>

        )
    }
}

export default SeachPage;

Upvotes: 1

Minh Nguyen Quang
Minh Nguyen Quang

Reputation: 71

You console.log props value on contructor method => the data display is first data. You should try console.log on render method. Goodluck!

Upvotes: 1

Related Questions