React: How to pass data between pages? - javascript

Hello and thank you for your time.
I am following this course: https://app.pluralsight.com/library/courses/react-flux-building-applications/table-of-contents and I have discovered that the React Router API has changed.
I am facing difficulties when trying to pass form's data to view, through Route, and Link components.
I will write the important code:
Main.js holds the Route, the important Route is the one which has /author/:id
const Main = () => (
<Switch>
<Route exact path="/Home" component={Home}/>
<Route path="/authors" component={AuthorPage}/>
<Route path="/about" component={About}/>
<Route path="/author" component={ManageAuthorPage}/>
<Route path="/author/:id" component={ManageAuthorPage}/>
<Redirect from="*" to="/Home"/>
</Switch>
);
We put the author.id from the authorList.js, the important part is: <td><Link to="/author/"render={(props) => <ManageAuthorPage id={author.id}/>}>{author.id}</Link></td>
const AuthorList = (props) => {
const createAuthorRow = function (author) {
return (
<tr key={author.id}>
<td><Link to="/author/"
render={(props) => <ManageAuthorPage id={author.id}/>}>{author.id}</Link>
</td>
<td>{author.firstName} {author.lastName}</td>
</tr>
)
;
};
return (
<div>
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
{props.authors.map(createAuthorRow, this)}
</tbody>
</table>
</div>
);
};
AuthorList.propTypes = {
authors: PropTypes.array.isRequired
};
And we use it, on the manageAuthorPage.js, the important code is in ComponentWillMount:
class ManageAuthorPage extends React.Component {
state = {
author: {
id: '',
firstName: '',
lastName: '',
},
};
setAuthorState = (event) => {
let field = event.target.name;
let value = event.target.value;
this.state.author[field] = value;
return this.setState({author: this.state.author});
};
saveAuthor = (event) => {
event.preventDefault();
AuthorApi.saveAuthor(this.state.author);
toastr.success('Author saved ;=)');
};
componentWillMount = () => {
const authorId = this.props.id;
console.log(authorId);
if (authorId) {
this.setState({author: AuthorApi.getAuthorById(authorId)});
}
};
render() {
return (
<AuthorForm author={this.state.author}
onChange={this.setAuthorState}
onSave={this.saveAuthor}/>
);
};
}
Also the console outputs:
Warning: Invalid value for prop render on tag. Either remove it from the element, or pass a string or number value to keep it in the DOM. For details.
Could you help me please 😉😉😉?
I have also read: React Router Pass Param to Component
https://medium.com/#pshrmn/a-simple-react-router-v4-tutorial-7f23ff27adf
EDIT:
I have also tried #Ramana Venkata suggestion, using:
<Route path="/author?id={id}" component={ManageAuthorPage}/>
And in manageAuthorPage I do:
componentDidMount = () => {
const authorId = this.props.id;
console.log(this.props);
if (authorId) {
this.setState({author: AuthorApi.getAuthorById(authorId)});
}
};
And I do see the author.id in the url, but it does not spawn when we write: console.log(this.props);
So I mean, if the URL is:
http://localhost:3001/author/cory-house
The console output for match.location.search is "". Why?
Thank you for your help.
EDIT2: I have also tried updated course's version:
https://github.com/coryhouse/react-flux-building-applications/pull/1/files
With:
The same Main.js route:
<Route path="/author/:id" component={ManageAuthorPage}/>
Passing the id into the link using it like a string:
<td><Link to={"author/" + author.id}>{author.id}</Link></td>
Also in manageAuthorPage we now Have:
componentDidMount = () => {
var authorId = this.props.match.params.id; //from the path '/author:id'
console.log(this.props);
};
And authorId is undefined:
Also in the console's output I only see the author.id in the: match.location.pathname, for URL: http://localhost:3001/author/cory-house we see:
"/author/cory-house"
Could you help me please?
Thank you.

Related

Reac router component in a list as a state

does anyone have experience with React routers?Is there a way to create a list of routes and hold it in a usestate? When i try to do the [... prevCountryRoutes] i get the error that prevCountryRoutes is not iterable
const [countriesWithRouteList,setCountriesWithRouteList]=React.useState(JSON.parse(localStorage.getItem("countriesWithRouteList")) || [])
const [countryRoutes,setCountryRoutes]=React.useState()
function addCountryRoute(co){
if(countriesWithRouteList.filter(el => el == co) == null){
console.log('already route exists')
}else{
console.log('country page being added')
setCountryRoutes((prevCountryRoutes)=>{
const newRoute = <Route
key={nanoid()}
path={`/countrypage/${co.replace(/ /g, '%20')}`}
element={
<CountryPage
country={co}
holidays={holidays}
handleDeleteClick={handleDeleteClick}
handleFormSubmit={handleFormSubmit}
/>
}
/>
return(
[...prevCountryRoutes, newRoute]
)
})
}
setCountriesWithRouteList(prevList => [...prevList, co])
}
The error you are asking about is cause by not declaring an initial countryRoutes state that is iterable. It's undefined.
Anyway, it's a React anti-pattern to store React components and JSX in state. Just store the data and render the derived JSX from state.
I suggest the following refactor:
const [countries, setCountries] = React.useState(JSON.parse(localStorage.getItem("countries")) || []);
function addCountryRoute(co){
if (countries.some(el => el.co === co)){
console.log('already route exists')
} else {
console.log('country page being added')
setCountries((countries) => countries.concat({
id: nanoid(),
co,
}));
}
}
...
{countries.map(country => (
<Route
key={country.id}
path={`/countrypage/${co.replace(/ /g, '%20')}`}
element={
<CountryPage
country={co}
holidays={holidays}
handleDeleteClick={handleDeleteClick}
handleFormSubmit={handleFormSubmit}
/>
}
/>
))}
And instead of mapping a bunch of routes that differ only in the country path segment, render just a single route where the country code is a route path parameter and the CountryPage component uses the useParams hook to get the code.
Example:
<Route
path="/countrypage/:country"
element={
<CountryPage
holidays={holidays}
handleDeleteClick={handleDeleteClick}
handleFormSubmit={handleFormSubmit}
/>
}
/>
CountryPage
const { country } = useParams();
Initialize countryRoutes with an array, so the first time can be iterable.
const [countryRoutes,setCountryRoutes] = React.useState([])

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)

Component only rending if I start the flow from the homepage

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.

React state variable is undefined inside render method

I have a state variable dataSource that has some data in it.
In a parent component I have the following:
updateFeed = newItem => {
this.setState({ dataSource: this.state.dataSource.data.unshift(newItem) })
console.log(this.state.dataSource.data)
}
render() {
console.log(this.state.dataSource.data)
return (
<React.Fragment>
<Header />
<Route
path="/"
exact
component={props => (
<Feed {...props} feedData={this.state.dataSource.data} updateFeed={this.updateFeed} />
)}
/>
<Route path="/profile/:id" exact component={props => <Profile {...props} />} />
</React.Fragment>
)
}
updateFeed is called from the child component.
onSubmit = () => {
const newPost = newData // some new data
this.props.updateFeed(newPost)
}
The updateFeed function is getting executed on submit, and the console.log is giving the updated data. But inside the render function this.state.dataSource.data is undefined. What am I missing here?
You do dataSource: dataSource.data in your setState call, therefore dataSource.data in your render method will actually access dataSource.data.data which is probably undefined. May change updatedFeed to:
updateFeed = newItem => {
this.setState(prev => ({
dataSource: {
...prev.dataSource,
data: prev.dataSource.data.concat(newItem)
}
}));
}
Which ensures a pure state.
It is because previously, this.state.dataSource is an object having key data. So even you are setting new value in updateFeed but in the very next line, state has not been updated yet. React does this asynchronously. so your log statement is showing old data.
You need to update state like this
const dataSource = this.state.dataSource;
dataSource.data.unshift(newItem);
this.setState({ dataSource: dataSource })

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