Component only rending if I start the flow from the homepage - javascript

I am having an issue with my application. My user component only loads UserCard when I start the application from the homepage then click users link there... if I just refresh the users URL... UserCard doesn't get loaded which means something is wrong with my this.props.users. I do see that in chrome it says: Value below was evaluated just now when I refresh but when I go through the flow it doesn't say that. Any help will be appreciated.
App.js
class App extends Component {
constructor(props) {
super(props);
this.state = {
users: []
};
}
componentDidMount() {
users = []
axios.get('/getall').then((res) => {
for(var d in res.data) {
users.push(new User(res.data[d]));
}
});
this.setState({ users });
}
render() {
const { users } = this.state;
return (
<Router history={history}>
<Switch>
<PrivateRoute exact path="/" component={Home} />
<Route exact path='/users' render={(props) => <Users {...props} users={users} />}/>
</Switch>
</Router>
)
}
}
PrivateRoute:
export const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
<Component {...props} /> )} />
)
User.js
export default class Users extends Component {
render() {
console.log(this.props.users);
return (
<Row>
{this.props.users.map(u =>
<UserCard key={u.name} user={u}/>
)}
</Row>
);
}
}
export class User {
constructor(obj) {
for (var prop in obj){
this[prop] = obj[prop];
}
}
getURLName() {
return this.name.replace(/\s+/g, '-').toLowerCase();
}
}
class UserCard extends Component {
render() {
return (
<Link to={'/users/' + this.props.user.getURLName()} >
<div>
// Stuff Here
</div>
</Link>
);
}
}

As per the comments:
The issue here is how you're setting state. You should never modify state directly since this will not cause the component to rerender See the react docs
Some additional thoughts unrelated to the question:
As per the comments - use function components whenever possible, especially with hooks on the way
There is probably no need to create a User class, only to new up little user objects. Simply use plain old JS objects and calculate the link url right in the place its used:
render() {
const { user } = this.props
return <Link to={`/users/${user.name.replace(/\s+/g, '-').toLowerCase()}`} />
}
It might be a good idea to start using a linter such as eslint. I see that you're declaring users = [] without using let or const (don't use var). This is bad practice since creating variables in this way pollutes the global name space. Linters like eslint will help you catch issues like this while you're coding.

Related

How to share props to a different route using react-router-dom?

I am trying to share my props (data, saveWorkButtonClicked, updateFBRDB) from <ProjectPage /> component route to <Indent /> component route.
But getting the following error:
Uncaught DOMException: Failed to execute 'pushState' on 'History': async (data, setSpinner, updateFBRDB) => {
setSpinner && setSpinner(true);
let rawRoomData = String.raw`${J...<omitted>...
} could not be cloned.
App.js
<Router>
<Switch>
<Route path="/ProjectPage/:projectId" exact component={ProjectPage} />
<Route path="/Indent/" render={(props) => <Indent {...props} />} />
</Switch>
</Router>
ProjectPage.js
history.push("/Indent/",
{
data: { ...project, rooms: project.rooms, ProjectId: project.ProjectId, ClientName: project.ClientName, Address: project.Address, AmountRecieved: project.AmountReceived, SiteEngineerId: project.SiteEngineersId },
saveWorkButtonClicked,
updateFBRDB,
}
)
// saveWorkButtonClicked & updateFBRDB are API calls which will be called in <Indent />
Indent.js
export default function Indent({ data, saveWorkButtonClicked, updateFBRDB }) {
console.log('data in indent', data)
}
NOTE: Please give solutions where this can be implemented without Context/ Redux/ Mobx. Also, I am using react-router-dom v5.2.0
I would suggest an workaround. Have a state which keeps track of when you want to move to next page, so that we can use Redirect component conditionally with your desired data as props.
App.js
<Router>
<Switch>
<Route path="/ProjectPage/:projectId" exact component={ProjectPage} />
</Switch>
</Router>
ProjectPage.js
const [isDone, setIsDone] = useState(false);
const handleClick = () => {
// Do all your works, when you want to `push` to next page, set the state.
setIsDone(true);
}
if(isDone) {
return (
<>
<Route path="/Indent"
render={ props =>
<Indent
{...props}
data={...}
saveWorkButtonClicked={saveWorkButtonClicked}
updateFBRDB={updateFBRDB}
/>
}
/>
<Redirect to="/Indent" />
</>
);
}
return (
<div>Your Normal Profile Page goes here</div>
)
If you want to "share" props, you need to do one of two things. Either have the receiving component be a child of the propsharing component - in which case you can pass them as props directly. Else, you would need to pass them as state via a common ancestor component, which you would need to update by sending a callback down to the component that will update the state.
You can pass state to location with this format
const location = {
pathname: '/Indent/',
state: {
data: { ...project, rooms: project.rooms, ProjectId: project.ProjectId, ClientName: project.ClientName, Address: project.Address, AmountRecieved: project.AmountReceived, SiteEngineerId: project.SiteEngineersId },
saveWorkButtonClicked,
updateFBRDB,
}
}
history.push(location)
And then using withRouter to receive location values
import { withRouter } from 'react-router'
function Indent({ location }) {
const { state } = location
const { data, saveWorkButtonClicked, updateFBRDB } = state || {}
return <></>
}
export default withRouter(Indent)

React: How to pass data to children

I am a beginner in React and what I am doing might not make sense.
What I am trying to do is just to pass a data to from a parent component which is used in every screen to children.
My code is like this.
AppDrawer.tsx
const AppDrawer: React: FC<Props> = ({
children,
}) => {
const [aString, setString] = React.useState<string>('Hello');
...
<div>
<Drawer>
...
</Drawer/>
<main>
<div>
<Container>
{children}
</Container>
</div>
</main>
</div>
App.tsx
<Swith>
<AppDrawer>
<Route path="/" component={ChildCompoent} />
...
...
</AppDrawer>
</Switch>
ChildComponent.tsx
export default class ChildComponent extends React.Component<Props, State> {
state = {
..
}
}
And now I want to access aString in AppDrawer.tsx in child components but I couldn't figure out how I can do it. How can I do it?
I think you can use Context here. Context allows you to pass down something from the parent component, and you can get it at the child component (no matter how deep it is) if you'd like.
I made an example link
You can read more about it here
Updated: I notice you use Route, Router, and I don't use in my codesandbox. However, it's fine though. The main idea is to use context :D
Use render in component Route
<Route
path='/'
render={(props) => (
<ChildCompoent {...props}/>
)}
/>
And should not props aString in component AppDrawer
I'm not sure about the version of react and if it is still supported but this how I did this in my app.
try checking for React. Children over here: https://reactjs.org/docs/react-api.html#reactchildren
see if it's suitable for your case.
render() {
const children = React.Children.map(this.props.children, child => {
return React.cloneElement(child, {
...child.props,
setHasChangesBeenMade: (nextValue) => this.setState({ hasChangesBeenMade: nextValue })
});
});
return (children[0]);
}
for you it should be something like this :
const AppDrawer: React: FC<Props> = ({
children,
}) => {
const [aString, setString] = React.useState<string>('Hello');
...
const childrens = React.Children.map(children, child => {
return React.cloneElement(child, {
...child.props,
aString: aString
});
});
<div>
<Drawer>
...
</Drawer/>
<main>
<div>
<Container>
{childrens[0]}
</Container>
</div>
</main>
</div>

React passing down hooks causes rerender and loss of focus on input

I've got a parent component in which I initialize some piece of state, which I then pass down to the children components so that they can update that. However, when the update is triggered, the component tree is re-rendered and my inputs lose focus. Adding a key did not help.
// App.tsx
export function App(props) {
const useVal = useState("");
return (
<Router>
<Switch>
<Route
exact
path="/"
component={() => (
<StartScreen
useVal={useVal}
/>
)}
/>
// ...
</Router>
);
}
// StartScreen.tsx
interface StartScreenProps {
useVal: [string, React.Dispatch<React.SetStateAction<string>>];
}
function bindState<T>(
[value, setState]: [T, React.Dispatch<React.SetStateAction<T>>]
) {
return {
value,
onChange: ({ value }: { value: T }) => setState(value)
}
}
export const StartScreen = (props: StartScreenProps) => {
return (
<form>
<InputField
key="myInput"
{...bindState(props.useVal)}
/>
</form>
);
}
So, now when I start typing on my InputField (which is basically a wrapper on an <input>) on StartScreen.tsx, the input constantly loses focus as the component is totally re-rendered (I can see it in the DOM).
This happens because you are passing a function to the Route's component prop (I assume you are using react-router-dom) :
From the docs :
If you provide an inline function to the component prop, you would
create a new component every render. This results in the existing
component unmounting and the new component mounting instead of just
updating the existing component.
To solve this problem use the render prop :
<Route
exact
path="/"
render={() => (
<StartScreen
useVal={useVal}
/>
)}
/>
This allows for convenient inline rendering and wrapping without the
undesired remounting explained above.

how to Stop rerendering of entire component onChange event on input text field in reactJs

I m new to reactJs and i m creating user Authentication functionality. I have two components one is header which has navbar and it contains react-router routers and the other is login component which has two input fields ... The problem with login component is when i start typing in input field it loses focus after each character typed i know it is rerendering the whole component but i don't know how to solve this problem
header.js
changeName = (e) => {
this.setState({name : e.target.value})
}
changePass = (e) => {
this.setState({password:e.target.value})
}
login = () => {
var name = this.state.name;
var password = this.state.password
var mysession;
$.ajax({
url : 'http://localhost:4000/login',
type : "POST",
data : {username:name,password:password},
success : function(data){
if(data == true){
this.setState({sessionFlag:true})
$('#home')[0].click();
}
else {
this.setState({sessionFlag:false})
}
}.bind(this)
})
}
render(){
const {name,password} = this.state;
return (
<Router>
<div>
<Route path="/login" exact component={()=><Login
onClickHandle={this.login.bind(this)}
onChangeName={this.changeName.bind(this)}
onChangePass={this.changePass.bind(this)}
name={name}
password = {password} />} />
</div>
</Router>
)
}
login.js
render(){
return (
<form className="form-horizontal" method ="post">
<input
type="text"
onChange={this.props.onChangeName}
value={this.props.name}/>
<input type="text"
onChange={this.props.onChangePass}
value={this.props.password} />
<input type="button"
value="Login"
onClick={this.props.onClickHandle} />
</form>
)
}
The main issue is the manner in which you are specifying your Login component:
<Route
path="/login"
exact
component={() => (
<Login
onChangeName={this.changeName.bind(this)}
onChangePass={this.changePass.bind(this)}
name={this.state.name}
password={this.state.password}
/>
)}
/>
Using this syntax causes the child of the Route to look like a brand-new type of component with each rendering (since it will be a new arrow function instance each time) so the previous Login component will be completely unmounted and the new one mounted.
From https://reactrouter.com/web/api/Route/component:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use the render or the children prop (below).
Here is an example using the render-func approach:
Header.js
import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import Login from "./Login";
class Header extends React.Component {
constructor(props) {
super(props);
this.state = { name: "", password: "" };
this.changeName = this.changeName.bind(this);
this.changePass = this.changePass.bind(this);
}
changeName = (e) => {
this.setState({ name: e.target.value });
};
changePass = (e) => {
this.setState({ password: e.target.value });
};
render() {
return (
<Router>
<div>
<div>
<Link to="/login">Login</Link>
</div>
<Route
path="/login"
exact
render={() => (
<Login
onChangeName={this.changeName}
onChangePass={this.changePass}
name={this.state.name}
password={this.state.password}
/>
)}
/>
</div>
</Router>
);
}
}
export default Header;

React with react-router-dom 4 -- Cannot read property 'params' of undefined

I've been working on learning React to see if it suits my organization's needs, so needless to say I'm new at it. I've got a sample app that I've been working on to see how it works. I've gone through several of the answers here and haven't found one that fixes my problem.
I'm running into the problem where I get a "Uncaught (in promise) TypeError: Cannot read property 'params' of undefined" in the "componentDidMount()" at "const { match: { params } } = this.props;" method in the component below. I have a very similar component that takes an id from the url, using the same method, and it works fine. I'm confused as to why one is working and another isn't. I'm probably just making a rookie mistake somewhere (perhaps more than one), any hints/answers are appreciated.
The routing:
class App extends Component {
render() {
return (
<BrowserRouter>
<div>
<Route path='/' component={BaseView} />
<Route path='/test' component={NameForm} />
<Route path='/home' component={Home} />
<Route path='/quizzes' component={ViewQuizzes} />
<Route path='/comment/:rank' component={GetCommentsId} /*The one that works*//>
<Route path='/comment/edit/:testid' component={GetCommentEdit} /*The one I'm having trouble with*//>
<Route path='/comments' component={GetCommentsAll} />
</div>
</BrowserRouter>
);
}
}
The working component:
class GetCommentsId extends Component{
constructor (props) {
super(props)
this.state = {
Comments: [],
output: "",
wasClicked: false,
currentComment: " ",
}
this.handleCommentChange = this.handleCommentChange.bind(this);
}
componentDidMount(){
const { match: { params } } = this.props;
const url = 'http://localhost:51295/api/Values/' + params.rank;
axios.get(url).then(res => {
const comments = res.data;
this.setState({ comments });
this.output = (
<div>
<ul>
{ this.state.comments.map
(
comment =>
(<Comment
QuizId = {comment.Rank}
FirstName = {comment.FirstName}
Comments = {comment.Comments}
TestId = {comment.TestimonialId}
/>)
)}
</ul>
</div>
);
//console.log("From did mount: " + this.currentComment);
this.forceUpdate();
});
}
componentDidUpdate(){}
handleCommentChange(event){
//console.log("handle Comment Change activated");
}
handleClick(comment){
this.wasClicked = true;
this.currentComment = comment.Comments;
console.log(comment.Comments);
this.forceUpdate();
}
render () {
if(this.output != null){
if(!this.wasClicked){
return (this.output);
}
else{
console.log("this is the current comment: " + this.currentComment);
return(
<div>
{this.output}
<NameForm value={this.currentComment}/>
</div>
);
}
}
return ("loading");
}
}
The one that isn't working:
class GetCommentEdit extends Component {
constructor (props) {
super(props)
this.state = {
Comments: [],
output: "",
match: props.match
}
}
componentDidMount(){
const { match: { params } } = this.props;
const url = 'http://localhost:51295/api/Values/' + params.testid;
axios.get(url).then(res => {
const comments = res.data;
this.setState({ comments });
this.output = (
<div>
<ul>
{ this.state.comments.map
(comment =>
(<EditComment
QuizId = {comment.Rank}
FirstName = {comment.FirstName}
Comments = {comment.Comments}
TestId = {comment.TestimonialId}
/>)
)}
</ul>
</div>
);
//console.log("From did mount: " + this.currentComment);
this.forceUpdate();
});
}
render(){
return(
<div>
{this.output}
</div>
);
}
}
I've created a small app for you to demonstrate how to implement working react router v4.
On each route there is a dump of props, as you can see the params are visible there.
In your code I don't see why you are not using Switch from react-router v4, also your routes don't have exact flag/prop. This way you will not render your component views one after another.
Link to sandbox: https://codesandbox.io/s/5y9310y0zn
Please note that it is recommended to wrap withRouter around App component, App component should not contain <BrowserRouter>.
Reviewing your code
Please note that updating state triggers new render of your component.
Instead of using this.forceUpdate() which is not needed here, update your state with values you get from resolving the Promise/axios request.
// Bad implementation of componentDidMount
// Just remove it
this.output = (
<div>
<ul>
{this.state.comments.map
(
comment =>
(<Comment
QuizId={comment.Rank}
FirstName={comment.FirstName}
Comments={comment.Comments}
TestId={comment.TestimonialId}
/>)
)}
</ul>
</div>
);
//console.log("From did mount: " + this.currentComment);
this.forceUpdate();
Move loop function inside render method or any other helper method, here is code for using helper method.
renderComments() {
const { comments } = this.state;
// Just check if we have any comments
// Return message or just return null
if (!comments.length) return <div>No comments</div>;
// Move li inside loop
return (
<ul>
{comments.map(comment => (
<li key={comment.id}>
<Comment yourProps={'yourProps'} />
</li>
))}
</ul>
)
};
Add something like isLoading in your initial state. Toggle isLoading state each time you are done with fetching or you begin to fetch.
this.setState({ isLoading: true }); // or false
// Initial state or implement in constructor
state = { isLoading: true };
Render method will show us loading each time we are loading something, renderComments() will return us comments. We get clean and readable code.
render() {
if (isLoading) {
return <div>Loading...</div>
}
return (
<div>
{this.renderComments()}
</div>
);
}

Categories