I am new in React and I will appreaciate much any help. I am using create-react-app, react-router-dom and express server. When I try to submit a comment to a blog post (child component called Details), it gets stored in the database, however the component does not seem to update and i do not see the new comment.As a result, I can see the new comment only after i refresh the page but not on form submit. I guess I am not setting componentDidUpdate properly but I do not have a clue how to do it, so i can see the comment immediately.
Here is my App.js:
class App extends Component {
constructor(props) {
super(props)
this.state = {
userId: null,
username: null,
isAdmin: false,
isAuthed: false,
jwtoken: null,
posts: [],
filtered: [],
}
this.handleSubmit = this.handleSubmit.bind(this)
}
static authService = new AuthService();
static postService = new PostService();
static commentService = new CommentService();
componentDidMount() {
const isAdmin = localStorage.getItem('isAdmin') === "true"
const isAuthed = !!localStorage.getItem('username');
if (isAuthed) {
this.setState({
userId: localStorage.getItem('userId'),
username: localStorage.getItem('username'),
isAdmin,
isAuthed,
})
}
this.getPosts()
}
componentDidUpdate(prevProps, prevState, posts) {
if (prevState === this.state) {
this.getPosts()
}
}
handleChange(e, data) {
this.setState({
[e.target.name]: e.target.value
})
}
handleCommentSubmit(e, data) {
e.preventDefault();
e.target.reset();
App.commentService.createComment(data)
.then(body => {
this.getposts()
if (!body.errors) {
toast.success(body.message);
}
else {
toast.error(body.message);
}
}
)
.catch(error => console.error(error));
}
getPosts() {
App.postService.getPost()
.then(data => {
this.setState({
posts: data.posts.length? data.posts : []
});
}
)
.catch(e => this.setState({ e }))
}
render() {
return (
<Fragment>
<Header username={this.state.username} isAdmin={this.state.isAdmin} isAuthed={this.state.isAuthed} logout={this.logout.bind(this)} />
<Switch>
<Route exact path="/" render={(props) => (
<Home
posts={this.state.posts}
handleSearchSubmit={this.handleSearchSubmit.bind(this)}
handleChange={this.handleSearchChange.bind(this)}
{...props} />
)} />
<Route path="/posts/:id" render={(props) =>
<Details handleSubmit={this.handleCommentSubmit.bind(this)}
isAdmin={this.state.isAdmin}
isAuthed={this.state.isAuthed}
posts={this.state.posts}
handleChange={this.handleChange}
{...props} />} />
</Switch>
<Footer posts={this.state.posts} formatDate={this.formatDate} />
</Fragment>
);
}
}
export default withRouter(App);
Here is my Details.js:
class Details extends Component {
constructor(props) {
super(props);
this.state = {
post: null,
comment: null
}
this.handleChange = props.handleChange.bind(this);
}
componentDidMount() {
const { posts, match } = this.props;
this.setState({
post: posts.length
? posts.find(p => p._id === match.params.id)
: null,
userId: localStorage.getItem('userId')
})
}
componentDidUpdate(prevProps) {
const { posts, match, isAuthed } = this.props;
if (JSON.stringify(prevProps) === JSON.stringify(this.props)) {
return;
}
this.setState({
post: posts.length
? posts.find(p => p._id === match.params.id)
: null
});
}
render() {
const { post } = this.state;
const { isAdmin, isAuthed } = this.props;
if (!post) {
return <span>Loading post ...</span>;
}
return (
<section className="site-section py-lg">
<form onSubmit={(e)=> this.props.handleSubmit(e, this.state)} className="p-5 bg-light">
<div className="form-group">
<label htmlFor="message">Message</label>
<textarea name="comment" id="message" onChange={this.handleChange} cols={30} rows={10} className="form-control" defaultValue={ ""} />
</div>
<div className="form-group">
<input type="submit" defaultValue="Post Comment" className="btn btn-primary" />
</div>
</form>}
</section>
);
}
}
export default Details;
Any help will be much appreciated!
You are doing a mistake that will be done by any new React developer. Just remember one thing that:-
UI is a function of state
So your UI will only be updated if your state is update.
After submitting a comment don't fetch all your comments again, just concat your new comment to current state and you will see your comment as soon as you submit it successfully
Related
I'm trying to implement firebase authentication with google to my app. And I use context to share data between screens. But I ran into problem of too many re-render.
export const UserContext = createContext({user: null});
class UserProvider extends Component {
constructor() {
super();
this.state = {
user: null,
};
}
componentDidMount = () => {
auth.onAuthStateChanged((userAuth) => {
if (!!userAuth) {
console.log("signed in");
console.log(userAuth);
this.setState({user: userAuth});
} else {
console.log("not signed in");
}
});
};
render() {
return (
<UserContext.Provider value={this.state.user}>
{this.props.children}
</UserContext.Provider>
);
}
}
export default UserProvider;
Here's the Application.jsx
return (
<>
<NavBar onHamburgerClick={handleHamburgerClick} showHamburger={true} />
<main>
<LeftMenu visible={leftMenuVisible} showScheduler={true} />
<Router className="main-container">
<Home path="/" />
<NewTimeTable path="new" />
<TeacherTimeTable path="teacher-time-table" />
<AllocateManagement path="allocate-manage" />
<ScheduleTimeTable path="schedule-timetable" />
</Router>
</main>
</>
);
And here's the App.js
const App = () => {
return (
<UserProvider>
<Application />
</UserProvider>
);
};
Also I'm using #reach/router
The problem here I think is because of setState I put inside componentDidMount. The variable userAuth is totally fine. I just can't set it to this.state.user
I don't know what exactly happened but my app works fine now.
export const UserContext = createContext({
user: null,
});
class UserProvider extends Component {
constructor() {
super();
this.state = {
user: {},
};
// set new user
this.setUser = (newUser) => {
this.setState({user: newUser}, () => {
console.log("new user: ");
console.log(this.state.user);
});
};
}
componentDidMount = () => {
// check if the user has changed and setState to the {user}
auth.onAuthStateChanged((userAuth) => {
if (!!userAuth) {
console.log("signed in");
this.setUser(userAuth);
} else {
console.log("not signed in");
}
});
};
render() {
return (
<UserContext.Provider value={this.state.user}>
{this.props.children}
</UserContext.Provider>
);
}
}
export default UserProvider;
I tweak something and it worked! Trying to figure it out
I have an App.jsx parent component and a TopBar.js child. What I would like to do is get the appId parameter from the url and pass it into my TopBar child component. The problem is that I don't know how to use match.params.appId like CategoryPage (<CategoryPage categoryId = {match.params.categoryId} />) because I get an error message: match is not defined, which seems normal since my child component is not included in a <Route> component. I looked at the documentation and I can only find the classic case, is there another way to retrieve a route parameter and for example store it in a state in order to reuse it?
Thank you in advance for any help or advice, I am new to this project and I am gradually integrating the particularities of the code.
App.jsx
export default class App extends PureComponent {
static childContextTypes = {
apiKEY: PropTypes.string,
apiURL: PropTypes.string,
appName: PropTypes.string,
loginToken: PropTypes.string,
userId: PropTypes.string,
};
constructor(props) {
super(props);
const parsed = queryString.parse(window.location.search);
const state = {
apiKey: null,
appName: null,
fetchApiKeyError: null,
fetchApiKeyPending: false,
fetchApiKeyDone: false,
};
['auth_token', 'userId'].forEach((key) => {
state[key] = parsed[key] || localStorage.getItem(key);
if (parsed[key]) localStorage.setItem(key, parsed[key]);
});
this.state = state;
this.handleErrorAuth = this.handleErrorAuth.bind(this);
}
getChildContext() {
const {
auth_token: loginToken, userId, apiKey, appName,
} = this.state;
return {
apiURL: process.env.REACT_APP_API_URL,
loginToken,
userId,
apiKEY: apiKey,
appName,
};
}
renderRedirect = () => {
const isLogged = localStorage.auth_token && localStorage.userId;
// This is a private app, so we need to be logged all time
if (!isLogged) {
window.location = `${process.env.REACT_APP_AUTH_URL}?redirect_uri=${window.location}`;
return null;
}
return null;
}
fetchApiKey = (appId, authToken) => {
if (!authToken) return;
this.setState({ fetchApiKeyPending: true });
const storageKey = `apiKey_${appId}`;
const nameStorageKey = `name_${appId}`;
const apiKey = localStorage.getItem(storageKey);
const appName = localStorage.getItem(nameStorageKey);
// ApiKey and appName already in localStorage
if (apiKey && appName) {
this.setState({
fetchApiKeyPending: false,
fetchApiKeyDone: true,
apiKey,
appName,
});
return;
}
// flush all previous keys
Object.keys(localStorage)
.filter((val) => val.indexOf('apiKey_') + 1 || val.indexOf('name_') + 1)
.forEach((val) => localStorage.removeItem(val));
// get ApiKey
fetch(`${process.env.REACT_APP_API_URL}/apps/${appId}/infos`, {
headers: {
Authorization: `Bearer ${authToken}`,
},
}).then((data) => {
if (!data.ok) throw new Error(data.status);
return data;
})
.then((data) => data.json())
.then(({ key, name }) => {
localStorage.setItem(storageKey, key);
localStorage.setItem(nameStorageKey, name);
this.setState({
fetchApiKeyPending: false,
fetchApiKeyDone: true,
fetchApiKeyError: null,
apiKey: key,
appName: name,
});
})
.catch((e) => this.setState({
fetchApiKeyPending: false,
fetchApiKeyDone: true,
fetchApiKeyError: e,
apiKey: null,
appName: null,
}));
};
getLastAppId = () => {
const storageKey = Object.keys(localStorage).filter(
(val) => val.indexOf('apiKey_') + 1,
)[0];
return storageKey ? storageKey.split('apiKey_')[1] : null;
};
switch = (location) => {
const {
fetchApiKeyPending,
fetchApiKeyDone,
apiKey,
auth_token: loginToken,
fetchApiKeyError,
} = this.state;
return (
<Switch location={location}>
{apiKey && [
<Route
key="1"
path="/:appId/categories/:categoryId/new_article"
render={({ match }) => (
<DefaultLayout>
<NewArticlePage categoryId={match.params.categoryId} />
</DefaultLayout>
)}
/>,
<Route
key="2"
path="/:appId/articles/:articleId"
render={({ match }) => (
<DefaultLayout>
<ModifyArticlePage articleId={match.params.articleId} />
</DefaultLayout>
)}
/>,
<Route
key="3"
path="/:appId/createCategory"
render={({ match }) => (
<CenteredLayout
backButtonProps={{
to: `/${match.params.appId}/categories`,
}}
>
<NewCategoryPage />
</CenteredLayout>
)}
apikey
/>,
<Route
key="4"
path="/:appId/categories/:categoryId/modify"
render={({ match }) => (
<CenteredLayout>
<NewCategoryPage categoryId={match.params.categoryId} />
</CenteredLayout>
)}
/>,
<Route
key="5"
path="/:appId/categories/:categoryId"
render={({ match }) => (
<CenteredLayout
backButtonProps={{
to: `/${match.params.appId}/categories`,
}}
>
<CategoryPage categoryId={match.params.categoryId} />
</CenteredLayout>
)}
/>,
<Route key="6" path="/:appId/categories" component={CategoriesPage} />,
]}
<Route path="/welcome" component={WelcomePage} />
<Route path="/app_not_found" render={() => <AppNotFoundPage titleKey="press" />} />
<Route
path="/:appId/"
exact={false}
render={({ match }) => {
if (fetchApiKeyError) {
return <Redirect to="/app_not_found" />;
}
if (!fetchApiKeyDone || fetchApiKeyPending) {
return (
[
<OnMountExecuter
key="1"
execute={this.fetchApiKey}
params={[match.params.appId, loginToken]}
/>,
<Loading key="2" style={{ fontSize: 36 }} />,
]
);
}
return <Redirect to={`/${match.params.appId}/categories`} />;
}}
/>
<Route
path="/"
render={() => {
if (localStorage.auth_token && localStorage.userId) {
const appId = this.getLastAppId();
if (appId) {
return <Redirect to={`/${appId}/categories`} />;
}
}
return <Redirect to="/welcome" />;
}}
/>
</Switch>
);
};
handleErrorAuth() {
this.setState({
auth_token: null,
userId: null,
});
localStorage.clear();
}
renderContent(location) {
return (
<HttpsRedirect>
<IntlProvider locale={language} messages={messages[language]}>
<div id="container">
<div id="content-container">
<AuthorizeChecker onError={this.handleErrorAuth} />
<UserContextProvider>
<UserContext.Consumer>
{(user) => <TopBar user={user} />}
</UserContext.Consumer>
{this.switch(location)}
</UserContextProvider>
</div>
</div>
</IntlProvider>
</HttpsRedirect>
);
}
render() {
return (
<BrowserRouter>
<Route
render={({ location }) => this.renderRedirect(location) || this.renderContent(location)}
/>
</BrowserRouter>
);
}
}
You could access the match object from any grand child component of a Router component using the context created by the react router, it is found under, context =>router=>match. From there you can do whatever you want with it.
You can access the context under this.context in any class based component, in a hooks component you have to use the hook useContext
Also note that newer versions of react router have hooks that might help you, like
useRouteMatch and useParams.
I have a use case where the user gets to log in. As soon as the user gets log in, another component should be shown. This is not working to me. I have to hit the login button again to show another component or has to refresh the page.
Here is what I have done
This is the parent component
const mapStateToProps = state => {
return {
user: state.loginReducer
}
}
class App extends React.Component {
state = {
hasToken: false
}
componentDidMount() {
const { user } = this.props;
window.chrome.storage.sync.get(['user_token'], result => {
if ((user && user.access_token) || result.user_token) {
console.log('user_token in cdm', result, user);
this.setState({ hasToken: true })
}
})
}
componentWillReceiveProps(nextProps) {
if (this.props.user !== nextProps.user) {
window.chrome.storage.sync.get(['user_token'], result => {
if (nextProps.user.length || result.user_token) {
this.setState({ hasToken: true })
}
})
}
}
anotherComponent() { // just to show the demo
return (
<div class="content">
component to show when the user logs in
</div>
)
}
render() {
const { hasToken } = this.state;
return (
<div>
<Header />
{ !hasToken ? <Login /> : this.anotherComponent()}
</div>
)
}
}
export default connect(mapStateToProps, null)(App);
login.js
const mapDispatchToProps = dispatch => ({
userLogin: user => dispatch(login(user))
})
class Login extends React.Component {
state = {
user: {
email:"",
password: "",
grant_type: "password"
}
}
handleChange = e => {
this.setState({user: {...this.state.user, [e.target.name]: e.target.value}})
}
handleSubmit = e => {
e.preventDefault();
this.props.userLogin(this.state.user);
}
render() {
const { user } = this.state;
return (
<Grid>
<Row className="pad-10">
<Col sm={12} md={6} mdOffset={3}>
<Form onSubmit={this.handleSubmit}>
<FormGroup controlId="email">
<ControlLabel>Email</ControlLabel>
<FormControl
type="email"
placeholder="Email"
name="email"
onChange={this.handleChange}
value={user.email}
/>
</FormGroup>
<FormGroup controlId="password">
<ControlLabel>Password</ControlLabel>
<FormControl
type="password"
placeholder="Password"
name="password"
onChange={this.handleChange}
value={user.password}
/>
</FormGroup>
<FormGroup>
<Button type="submit">Sign in</Button>
</FormGroup>
</Form>
</Col>
</Row>
</Grid>
);
}
}
export default connect(null, mapDispatchToProps)(Login);
I am not using any router concept so What I wanted to do is when user hits the login button, if the login is successful, the token is respond from the server and that is checked so that if it is successful, the user will be shown another component.
UPDATE
export const login = action(LOGIN, 'user');
export const loginSuccess = action(LOGIN_SUCCESS, 'data');
export const loginFailure = action(LOGIN_FAILURE, 'error');
reducer code
const initialState = {
fetching: false,
error: null,
user: []
}
function loginReducer(state=initialState, action) {
switch (action.type) {
case LOGIN:
return {...state, fetching: true}
case LOGIN_SUCCESS:
return {...state, fetching: false, user: action.data.access_token}
case LOGIN_FAILURE:
return {...state, fetching: false, error: action.error}
default:
return state;
}
}
export default loginReducer;
I don't know exactly how does window.chrome.storage.sync works, but obvious solution (at the first glance) is:
// parent component
render() {
const { user } = this.props;
return (
<div>
<Header />
{ !user ? <Login /> : this.anotherComponent()}
</div>
)
}
You have to get user from your state
I'll provide more details when you bring your reducers/actions
The problem your code is not working is because the App component is only rendered once irrespective of weather the user is logged in or not. So later when the user is logged in, your app component is not re-rendered.
There could be many ways to solve this. What I suggest you to do is something like this:
pass an onUserLogin callback to Component something like
<LoginModal isModalOpen={isModalOpen} onLoginClick={this.onLoginClick} />
Then do a setState inside the onLoginClick function to make sure component is rendered with changed props.
onLoginClick = () => {
if (!this.state.isUserLoggedIn) {
this.setState({
isModalOpen:!this.state.isModalOpen,
});
}
}
I have a small React app which has a header component shared across the app from the index.js where the routing is set. I want to check on a specific page (Admin.js) if I'm logged in (Facebook auth already done with Firebase and working fine) and if so, show on the header component the log out button and Facebook profile pic.
index.js (imports omitted):
const Root = () => {
return (
<div>
<Header />
<main>
<Router>
<Switch>
<Route path="/" component={App} exact />
<Route path="/admin" component={Admin} exact />
</Switch>
</Router>
</main>
</div>
);
};
render(<Root/>, document.querySelector('#root'));
Header.js:
import React from 'react';
class Header extends React.Component {
render() {
return (
<header className="header">Header Home
<img src={image prop here or something...} alt=""/>
</header>
)
}
}
export default Header
Admin.js
class Admin extends React.Component {
constructor() {
super();
this.addPicture = this.addPicture.bind(this);
// getinitialstate
this.state = {
pictures: [],
uid: null,
avatar: ''
}
}
// firebase syncing
componentWillMount() {
this.ref = base.syncState('pictures', {
context: this,
state: 'pictures',
});
}
componentDidMount() {
firebase.auth().onAuthStateChanged((user) => {
if(user) {
this.authHandler(null, { user });
}
})
}
authenticate() {
firebase.auth().signInWithPopup(provider).then(() => {this.authHandler});
}
logout = () => {
firebase.auth().signOut().then(() => {
this.setState({
uid: null,
avatar: ''
});
});
}
authHandler(err, authData) {
console.log(authData)
if (err) {
return;
}
this.setState({
uid: authData.user.uid,
avatar: authData.user.photoURL
});
}
renderLogin() {
return (
<nav>
<h2>Please log in to access the Admin Area</h2>
<button className="c-form__btn" onClick={() => this.authenticate()}>Log in</button>
</nav>
)
}
addPicture(e) {
e.preventDefault();
const picsRef = firebase.database().ref('pictures');
const picture = {
title: this.title.value,
url: this.url.value,
category: this.category.value
}
picsRef.push(picture);
this.picForm.reset();
}
removePicture = (key) => {
const pictures = {...this.state.pictures};
pictures[key] = null;
this.setState({ pictures });
}
renderTable = () => {
return Object
.keys(this.state.pictures)
.map(key => <Table key={key} index={key} details={this.state.pictures[key]} removePic={() => this.removePicture(key)}/>)
}
render() {
const logout = <button className="c-form__btn secondary" onClick={this.logout}>Log Out!</button>
// check if ther're no logged id at all
if(!this.state.uid) {
return <div>{this.renderLogin()}</div>
}
// check if they are the owner of the app
if(this.state.uid !== USER_UID) {
return (
<div>
<h3>Access not allowed!</h3>
{logout}
</div>
)
}
return (
<div>
<h1>Admin</h1>
<p>{logout}</p>
<img src={this.state.avatar} alt="User" style={{width: '50px'}}/>
<form ref={(input) => this.picForm = input} className="c-form" onSubmit={(e) => this.addPicture(e)}>
<div className="c-form__field"><input ref={(input) => this.title =input} type="text" placeholder="title" className="c-form__input"/></div>
<div className="c-form__field"><input ref={(input) => this.url =input} type="text" placeholder="Image url" className="c-form__input"/></div>
<div className="c-form__field">
<select ref={(input) => this.category =input} className="c-form__input">
<option value=" " disabled>Select a category</option>
{catOptions()}
</select>
</div>
<div className="c-form__field"><button className="c-form__btn" type="submit">Add Item</button></div>
</form>
<div className="table">
<div className="table__row t_header">
{tableHeader()}
</div>
{this.renderTable()}
</div>
</div>
)
}
}
export default Admin
How do I show the logout button and the Facebook profile pic (this.state avatar) on the Header component?
You need to lift the state up. For example:
class Root extends React.Component {
state = {
isLogged: false
img: null,
username: '',
}
login = (username, img) => {
this.setState({
img,
username,
isLogged: true,
});
}
logout = () => {
this.setState({
img: null,
username: '',
isLogged: false,
})
}
render() {
return (
<div>
<Header
username={this.state.username}
img={this.state.img}
isLogged={this.state.isLogged}
logout={this.logout}
/>
<main>
<Router>
<Switch>
<Route path="/" component={App} exact />
<Route
exact
path="/admin"
render={props => <Admin {...props} login={this.login} />}
/>
</Switch>
</Router>
</main>
</div>
);
}
};
Now you can update state in Root with updater function passed to Admin. From now on you just pass props to whatever you want. You get the idea...
I'm passing a search input prop (searchTerm) to a React component (Graph).
It appears in dev tools that the Graph component is receiving the correct prop and state is being updated, but my fetch api function is not re-rendering the new data based on the updated prop. If I manually force data to the API url, the fetch works, so I know it has to be a way i'm passing the searchTerm. I've tried every iteration possible, but still can't get it to work. Any ideas?
class Graph extends Component {
constructor(props){
super(props);
this.state = {
loaded: false,
now: Math.floor(Date.now()/1000),
data1: [],
searchTerm: DEFAULT_QUERY
}
this.getData = this.getData.bind(this)
}
componentDidMount() {
const {now, searchTerm} = this.state;
this.getData(now, searchTerm);
}
componentWillReceiveProps(nextProps) {
this.setState({searchTerm: nextProps.searchTerm });
}
componentDidUpdate(prevProps) {
const {now, searchTerm} = this.state;
if(this.props.searchTerm !== prevProps.searchTerm) {
this.getData(now, searchTerm);
}
}
getData = (now=this.state.now, searchTerm=this.state.searchTerm) => {
let ticker = searchTerm.toUpperCase();
console.log(searchTerm);
fetch(`https://poloniex.com/public?
command=returnChartData¤cyPair=USDT_${ticker}&end=${now}&period=14400&start=1410158341`)
.then(res => res.json())
.then(results => {
this.setState({
data1:results.map(item => {
let newDate = (item.date)*1000; //*1000
return [newDate,item.close]
})
})
console.log(JSON.stringify(this.state.data1));
})
.then(()=> {
const {data1} = this.state;
this.setState({
min: data1[0][0],
max: data1[data1.length-1][0],
loaded: true})
})
})
}
render() {
const {data1, min, max} = this.state;
return (
<div className="graph">
<HighchartsStockChart>
<Chart zoomType="x" />
<Title>Highstocks Example</Title>
<Loading isLoading={!this.state.loaded}>Fetching data...</Loading>
<Legend>
<Legend.Title></Legend.Title>
</Legend>
<RangeSelector>
<RangeSelector.Button count={1} type="day">1d</RangeSelector.Button>
<RangeSelector.Button count={7} type="day">7d</RangeSelector.Button>
<RangeSelector.Button count={1} type="month">1m</RangeSelector.Button>
<RangeSelector.Button type="all">All</RangeSelector.Button>
<RangeSelector.Input boxBorderColor="#7cb5ec" />
</RangeSelector>
<Tooltip />
<XAxis min={min} max={max}>
<XAxis.Title>Time</XAxis.Title>
</XAxis>
<YAxis id="price">
<YAxis.Title>Price</YAxis.Title>
{this.state.loaded &&<AreaSplineSeries id="price" name="Price" data={data1} />}
</YAxis>
<Navigator>
<Navigator.Series seriesId="profit" />
</Navigator>
</HighchartsStockChart>
</div>
);
}
}