Reputation: 83
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:
App.js
(Handles login and routing) <SearchPage />
and <SearchResults />
<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:
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 , the props were displayed 4 times, and the last 2 times had partly correct info., 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
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
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