React Router sharing data between components - reactjs

I have a dynamic route set up in my react application, when a user clicks the image it navigates to a new route with url /details/:id:
<div className='App'>
<Switch>
<Route path='/' exact component={Home} />
<Route exact path='/details/:id' component={ItemDetails} />
</Switch>
</div>
It comes from my functional component:
const headerImages = (props) => {
const imageResults = props.trending.slice(0, 5).map(r => ( // Grab firt 5 array objects
<Link key={r.id} to={`/details/${r.id}`}>
<div key={r.id}>
<img key={r.id} src={`https://image.tmdb.org/t/p/w1280${r.backdrop_path}`} alt={r.title} className='header-image' />
<h1 className='now-playing'>Latest</h1>
<h1 className='header-title'>{r.title}</h1>
<h4 className='header-details'>{r.overview}</h4>
</div>
</Link>
))
return <div className='header-images'>
<Carousel infinite autoPlay={4500}>{imageResults}</Carousel>
</div>
}
export default headerImages
ItemDetails is a class based component that has an API call, How do I get r.id value from my functional component into my Api call?
class ItemDetails extends Component {
constructor (props) {
super(props)
this.state = { selectedItem: null }
}
fetchItemDetails = () => {
axios.get('https://api.themoviedb.org/3/movie/${this.props.r.id}?api_key=40d60badd3d50dea05d2a0e053cc96c3&language=en-US')
.then((res) => {
console.log(res.data.results)
})
}
componentDidMount(){
this.fetchItemDetails()
}
render () {
return <h1>test</h1>
}
}
Currently the API call returns undefined, but as you can see, I'm trying to pass a dynamic id into the call.
Updated solution:
class ItemDetails extends Component {
constructor (props) {
super(props)
this.fetchItemDetails = this.fetchItemDetails.bind(this)
}
fetchItemDetails = (itemId = this.props.match.params.id) => {
axios.get('https://api.themoviedb.org/3/movie/${itemId}?api_key=40d60badd3d50dea05d2a0e053cc96c3&language=en-US')
.then((res) => {
console.log(res.data.results)
})
}
componentDidMount(){
this.fetchItemDetails()
}
render () {
return <h1>test</h1>
}
}
export default ItemDetails

You can use:
const id = this.props.match.params.id
To get the id and get the data from that specific id.

This is because you haven't bound this to the function trying to access the props.
Add in the constructor:
this.fetchItemDetails = this.fetchItemDetails.bind(this);
and the url should use template literal ` instead of quote '
`https://api.themoviedb.org/3/movie/${itemId}?api_key=40d60badd3d50dea05d2a0e053cc96c3&language=en-US`

Try this:
import React, { Component } from 'react';
import axios from 'axios';
export default class ItemDtails extends Component {
fetchItemDetails = () => {
const itemId = this.props.match.params.id;
const ROOT_URL = 'https://api.themoviedb.org/3/movie';
const API_KEY = 'api_key=40d60badd3d50dea05d2a0e053cc96c3&language=en-US';
axios.get(`${ROOT_URL}/${itemId}?${API_KEY}`).then(res => {
console.log(res.data.results);
});
};
componentDidMount() {
this.fetchItemDetails()
}
render() {
return <div>Test...</div>;
}
}

Related

Can a single product detail page appear on the same page as the product list instead?

I'm tasked with using react to create our online store. So far I've succesfully called our products using the data from the API we're developing, and I've also been able to pass the data from the mapped product list to a single product page using a link.
Only issue now is that we'd like the single product to appear on the same page as the product list when it's clicked on by the user, perhaps as a component that appears above the product list (as opposed to linking to a separate page). For the life of me I cannot seem to find a method of doing this that doesn't result in an error or the app reading parameters as undefined. Admitedly, I am quite new to React, so it's possible I'm missing something very obvious.
This is the ProductList.js
import React, { useState, useEffect } from 'react';
import SingleProduct from './SingleProduct';
import { Link } from 'react-router-dom';
const API_URL = "http://exampleapiurl/ExampleCollection/Examplecollectionid";
const Products = () => {
const [products, setProducts] = useState([]);
useEffect(() => {
getProducts().then((products) => setProducts(products))
}, []);
const getProducts = () =>
fetch(API_URL)
.then((res) => res.json());
// OnClick Handler
const [isShown, setIsShown] = useState(false);
const handleClick = (e) => {
setIsShown(current => !current);
};
return (
<div className="GetProducts">
<h1> Fetch Products from a Collection </h1>
<div className="container">
<div className="row">
{/* 👇️ Ideally, we'd like the single product item to appear here on button click, as opposed to a separate page */}
{
isShown &&
<SingleProduct/>
}
{products.map((frame) => (
<div>
{/* 👇️ Current link to separate page for product*/}
<Link to={`/SingleProduct/${frame.frameId}`}>
{/* 👇️ Button to show single item on same page as product list.*/}
<button onClick={handleClick} value={frame.frameId} > View {frame.frameName}</button>
<div key={frame.frameId}>
<img src={`https://exampleimageurl/${frame.thumnail}`} />
<li>Frame Name: {frame.frameName}</li>
<li>Gender: {frame.gender}</li>
</div>
</Link>
</div>
))
}
</div>
</div>
</div>
)
}
export default Products;
This is the SingleProduct.js
class SingleProduct extends React.Component {
constructor(props) {
super(props)
this.state = {
isLoading: false,
item: [],
frameId: this.props.match.params.frameId
}
}
componentDidMount() {
this.setState({ isLoading: true });
fetch(`http://exampleapiurl/${this.state.frameId}`)
.then(response => response.json())
.then(json => {
this.setState({
item: json,
isLoading: false
})
})
}
render() {
const { item } = this.state;
return (
this.state.isLoading ?
(<h1>Loading {this.state.frameId}...</h1>)
:
(
<div>
<div className="col border text-center" key={item.frameId}>
<img src={`https://exampleimageurl/${item.framePic}`} />
<li>Frame Name: {item.frameName}</li>
<li>Gender: {item.gender}</li>
</div>
</div>
)
)
}
}
export default SingleProduct
App.js
import React, { Component } from 'react';
import { Route } from 'react-router';
import { Home } from './components/Home';
import { Layout } from './components/Layout';
import Products from './components/ProductList';
import SingleProduct from './components/SingleProduct';
export default class App extends Component {
static displayName = App.name;
render() {
return (
<Layout>
<Route exact path='/' component={Home} />
<Route path='/ProductList' component={Products} />
<Route path='/SingleProduct/:frameId' component={SingleProduct} />
</Layout>
);
}
}
So if I understand correctly you don't want to use route for passing the data instead of that you can then pass props to the SingleProduct component.
With props getting passed it should look
{
isShown &&
<SingleProduct frameId = {selectedFrameId}/>
}
Declare a new state variable to store the selected frameid
const [selectedFrameId, setSelectedFrameId] = useState<Number>();
Your onclick event will need adjustment, because you will need to pass the frameid in map function.
onClick={() => this.handleClick(frame.frameId)}
and then set the state via handleClick event
const handleClick = (frameId) => {
setIsShown(current => !current);
setSelectedFrameId(frameId);
};
With this in your SingleProduct component the fetch call can directly use the props(frameid)
fetch(`http://exampleapiurl/${this.props.frameId}`)
Also I would recommend to change SingleProduct to a functional component instead of class component.

Dynamically create React Routes Using Data From Component State

I am trying to dynamically create routes using react-router-dom. The route will depend on the id of a state I have in one of my components. How can I achieve this?
App.js:
function App() {
return (
<div className="App">
<Router>
<Route path="/" component={Categories}></Route>
<Route path="/:cat" component={Recipes}></Route>
</Router>
</div>
);
}
Categories.js which I want to get the id (:cat) from - the state categories has an id value:
class Categories extends React.Component {
state = {
categories: []
}
componentDidMount() {
fetch('http://localhost:8000/api/categories/')
.then(response => response.json())
.then(data => {
this.setState({categories: data});
});
}
render() {
return (
);
}
}
I have seen others use useParams but I can't do that since Categories is a class.
Thank you for the help.
Use
this.props.match.params
i.e:
import { BrowserRouter, Route, Router, Switch } from "react-router-dom";
import React from "react";
function App() {
return (
<div className="App">
<p>R</p>
<BrowserRouter>
<Switch>
<Route path="/categories/:id" component={Recipes}></Route>
<Route path="/" component={Categories}></Route>
</Switch>
</BrowserRouter>
</div>
);
}
class Categories extends React.Component {
state = {
categories: []
};
componentDidMount() {
fetch('http://localhost:8000/api/categories/')
.then(response => response.json())
.then(data => {
this.setState({categories: data});
});
}
render() {
return (
<div>
{this.state.categories.map((c) => (
<div>CAT: {c.name}</div>
))}
</div>
);
}
}
class Recipes extends React.Component {
state = {};
componentDidMount() {
console.log('Params:',this.props.match.params)
}
render() {
return <div></div>;
}
}
export default App;

React Router - Path with :id is not working correctly for component wrapped by HOC

Hi I have been developing this application using react and react-router-dom The Page component is wrapped by a HOC that imports a contentservice to access a rest api.
My navigation is in the App component. The relevant part is the
<Link to="/page/123">About Page</Link>
and
<Link to="/page/456">Contact Page</Link>
When these links are clicked the page doesn't redraw as i expected. First time i go to 'About Page' it's all good. Then when i click to go to 'Contact Page' nothing changes. Then i click on the 'About Page' again and the 'Contact Page' shows.
In all the cases above the browser address bar shows the right path and if i refresh the page i go to the right page.
Here is my navigation page:
import React, { Component } from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import { connect } from "react-redux";
import Home from "./Home";
import Admin from "./Admin";
import Members from "./Members";
import Login from "./Login";
import Page from "./Page";
import PrivateRoute from "./PrivateRoute";
import "./App.css";
class App extends Component {
render() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Home Page</Link>
</li>
<li>
<Link to="/page/123">About Page</Link>
</li>
<li>
<Link to="/page/456">Contact Page</Link>
</li>
<li>
<Link to="/members">Members</Link>
</li>
<li>
<Link to="/admin">Admin</Link>
</li>
</ul>
</div>
<Switch>
<Route path="/login" component={Login} />
<Route path="/page/:id" component={Page} />
<Route exact path="/" component={Home} />
<PrivateRoute path="/members">
<Members />
</PrivateRoute>
<PrivateRoute path="/admin">
<Admin />
</PrivateRoute>
</Switch>
</Router>
);
}
}
const mapStateToProps = (state) => {
return {
isLoggedIn: state.isLoggedIn,
};
};
export default connect(mapStateToProps, null)(App);
This is my page component:
import React, { Component } from "react";
import WithBackend from "./WithBackend";
class Page extends Component {
constructor(props) {
super(props);
this.resource = "/pages/";
this.state = { model: null };
}
render() {
if (this.state.model != null) {
return (
<div className="container">
<div className="row">
<div className="col-md">
<h1>{this.state.model.title}</h1>
<h2 dangerouslySetInnerHTML={{ __html: this.state.model.body }} />
</div>
</div>
</div>
);
} else {
return (
<div>
<h2>Page id: {this.props.match.params.id}</h2>
</div>
);
}
}
componentDidMount() {
this.props
.getEntity(this.resource, this.props.match.params.id)
.then((model) => this.setState({ model }));
}
componentDidUpdate(nextProps) {
if (nextProps.match.params.id !== this.props.match.params.id) {
this.props
.getEntity(this.resource, nextProps.match.params.id)
.then((data) => {
this.setState({ model: data });
});
}
}
}
export default WithBackend(Page);
This is the Withbackend HOC:
import React from "react";
import ContentService from "./ContentService";
const WithBackend = (WrappedComponent) => {
class HOC extends React.Component {
constructor() {
super();
this.contentService = new ContentService();
this.getEntity = this.getEntity.bind(this);
this.getEntities = this.getEntities.bind(this);
}
getEntity(resource, id) {
return this.contentService
.getEntity(resource, id)
.then((response) => response.json())
.catch((e) => {
console.log(e);
});
}
getEntities(resource) {
return this.contentService
.getEntities(resource)
.then((response) => response.json())
.catch((e) => {
console.log(e);
});
}
render() {
return (
<WrappedComponent
getEntity={this.getEntity}
getEntities={this.getEntities}
{...this.props}
/>
);
}
}
return HOC;
};
export default WithBackend;
And the content service:
class ContentService {
baseUrl = "http://localhost:8080";
getEntity(resource, id) {
const path = this.baseUrl + resource + id;
const fetchPromise = fetch(path, {
method: "GET",
});
return Promise.resolve(fetchPromise);
}
getEntities(resource) {
const fetchPromise = fetch(this.baseUrl + resource, {
method: "GET",
});
return Promise.resolve(fetchPromise);
}
}
export default ContentService;
Has anyone got any ideas why this is happening? I am not sure if it has anything to do with the Page component being wrapped by HOC but just thought it is worth mentioning.
Thank you.
Issue
The componentDidUpdate lifecycle method receives the previous props, state, and snapshot values, not the next ones.
componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot)
By sending the "previous" props' match param id you were a "cycle" behind.
Solution
Use the current id value from props.
componentDidUpdate(prevProps) {
if (prevProps.match.params.id !== this.props.match.params.id) {
this.props
.getEntity(this.resource, this.props.match.params.id)
.then((data) => {
this.setState({ model: data });
});
}
}

facing issue in passing state(loaded through api) from App Component through React Router

I am facing issue in passing state from App Component through React Router. In the App component's ComponentwillMount function, the state is loaded through an API, which is passed to Login Component by specifying it in the render function of the Route Component.
But, the Login Component is loaded prior to App setState. I need to pass this state to all other Components. Please help !
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
language: 'en',
labels: null,
};
}
componentDidMount() {
let language = getLanguage(); //from url
this.setState({ language }, async () => {
await this.getLabels();
});
}
getLabels = () => {
//Hit Api to fetch labels on basis of language set
this.setState({ labels: data });
};
render() {
return (
<div className='App'>
<Router>
<Switch>
<Route
exact
path='/'
render={(props) => (
<Login labels={this.state.labels} {...props} />
)}
/>
</Switch>
</Router>
</div>
);
}
}
export default App;
import React, { Component } from 'react';
export default class Login extends Component {
render() {
console.log(this.props.labels);
}
}
this.props.labels is undefined in Login Component.
Can you try showing a loder untill your api call was successfull.
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
language: 'en',
labels: null,
fetchingLabels:true
};
}
componentDidMount() {
let language = getLanguage(); //from url
this.setState({ language }, async () => {
await this.getLabels();
});
}
getLabels = () => {
//Hit Api to fetch labels on basis of language set
this.setState({ labels: data, fetchingLabels:false });
};
render() {
if(this.state.fetchingLabels){
return 'I am loading' // you can add your loader here
}
return (
<div className='App'>
<Router>
<Switch>
<Route
exact
path='/'
render={(props) => (
<Login labels={this.state.labels} {...props} />
)}
/>
</Switch>
</Router>
</div>
);
}
}
export default App;
import React, { Component } from 'react';
export default class Login extends Component {
render() {
console.log(this.props.labels);
}
}

Find Index of current route then pass index to component props using React JS and React Router

I have following the scenario where I have a few post links that route to a single page they belong to. Problem is, I am using the same component in the single page to display the photo but can't figure out how to pass the current index index so only the current indexed photo shows up. I am using react router and retrieving my data through an api call.
Single.js:
export default class single extends React.Component {
constructor() {
super();
}
static propTypes = {
posts: React.PropTypes.array.isRequired,
};
render() {
const { postId } = this.props.params;
const i = this.props.posts.findIndex((post) => post._id === postId);
const post = this.props.posts[i];
return (
<section className="blog-landing-page">
<Photo {...this.props} key={i} i={i} post={post} />
</section>
);
}
}
Photo.js
export default class posts extends React.Component {
constructor() {
super();
}
render() {
const { post, i} = this.props;
return (
<section>
<Link to={`/post/${post.key}`}>
<img src={post.image.url} alt='' className="grid-photo" />
<h1>{post.name}</h1>
</Link>
</section>
);
}
}
When console logging inside "single.js" I get the following output attached.
console.log(this.props.posts)
On your Route component you can access datas as params and then pass it down to the child.
class SomeRoute extends Component {
render () {
<Child photoId = {this.props.params.photoId} />
}
}
You just need to be sure to name the route param
<Route path="/foo/:photoId/bar" component={SomeRoute }/>

Resources