How to pass props to child component based on dynamic route - reactjs

I'm trying to make an ecommerce website with react and react-router-dom. I have different components: ProductsList, Product, Details (about single product).
I am using Context API in a separate file. My App.js file looks like this:
...imports
const products = useContext(ProductContext)
const App = () => {
...
<Route path="/products/:id">
<Details products={products} />
</Route>
}
But I want to send details only about the product whose id matches the dynamic route id, not the whole array of products. But how do I extract that id? I know I can do it inside the Details.jsx file by using useParams(). But how do I do that from inside App.js?
I just noticed there's a similar quesiton. But I want my Details component in its own file and not inside App.

You can try following:-
...imports
const products = useContext(ProductContext)
const App = () => {
...
<Route path="/products/:id" render={(props) => {
const id = props.match.params.id;
const productData = products.find(product => product.id === id);
return <Details {...props} product={productData} />
}}
</Route>
}

Related

How to pass id in Route React

I want to build a page when from list of products I want to see product by ID. In my App file I have something like that:
<Route path={ROUTES.product} element={<Product id={1} />}>
but I want to replace static id with the value that comes from the selected product. In my Product file I have redirection to page with product but I don't know how I can pass the ID.
onClick={() => { navigate(`/products/${product?.id}`)}}
Any help would be appreciated.
The code you've provided appears to pass the id value in the path. It seems your question more about getting the Product component to have the correct id prop passed to it.
Given: path is "/products/:id"
Options to access the id param:
Use the useParams hook in Product to read the id route path param. This only works if Product is a function component.
import { useParams } from 'react-router-dom';
...
const { id } = useParams();
...
<Route path={ROUTES.product} element={<Product />} />
Use a wrapper component to use the useParams hook and inject the id value as a prop. This is useful if Product is not a function component.
import { useParams } from 'react-router-dom';
const ProductWrapper = () = {
const { id } = useParams();
return <Product id={id} />
};
...
<Route path={ROUTES.product} element={<ProductWrapper />} />
Create a custom withRouter Higher Order Component to use the useParams hook and inject a params prop.
import { useParams, ...other hooks... } from 'react-router-dom';
const withRouter = Component => props => {
const params = useParams();
... other hooks...
return (
<Component
{...props}
params={params}
... other hooks ...
/>
);
};
...
Wrap Product with withRouter HOC and access id param from props.params
props.params.id // or this.props.params
...
export default withRouter(Product);
...
<Route path={ROUTES.product} element={<Product />} />
so you already have the id with this
navigate(`/products/${product?.id}`)
just in Product component you can access id with
const { id } = useParams();
if you need to pass extra data you can try:
navigate(`/product/${product?.id}`, { state: { someExtradata }};
that you can access state in the component with :
const { state } = useLocation();
onClick={ () => Navegar(id) }
function Navegar(id) {
navigate('/products/id)
}

Passing and spreading props in React Router Dom v 6

I have the below working in RRD v 5.3.0 but cant get an alternative version working in v6. Ive tried using 'element' but it doesn't seem to work
<BrowserRouter>
<Route render={props => (
<>
<Header {...props}/>
<Routes/>
<Footer/>
</>
)}/>
</BrowserRouter>
Version 6 no longer supports the render function style. Instead the preferred approach is to use the use hooks (eg, useLocation, useParams) in the child component to access whatever values are needed.
For example, if header was previously expecting to get location and match via its props:
const Header = ({ match, location }) => {
// ...
}
It would now do:
const Header = () => {
const location = useLocation();
const match = useMatch();
// ...
}
If header can't be modified for some reason, then you could make a component that uses these hooks, and then passes the values down to the header as props:
const Example = () => {
const location = useLocation();
const match = useMatch();
return (
<>
<Header location={location} match={match}>
<Routes/>
<Footer/>
<>
)
}
// used like:
<Route>
<Example/>
</Route>
For a guide on updating from version 5 to 6, see this page
For information about why they made this change, see this article

Can I create 2 components which share a state?

In my React app, I have a layout file, and I want to be able to pass 2 different components into it. One component is to be shown in Area 1, and another is to be shown in Area 2. Both components need to share information with each other.
So, my layout is:
const SplitLayout = (Area1Content, Area2Content) => {
return (
<div className="area1">
<Area1Content />
</div>
<div className="area2">
<Area2Content />
</div>
);
}
export default SplitLayout;
In my App.js I have:
const App = () => (
<Router>
<Switch>
<Route exact path={ROUTES.HOME}
render={(props) =>
<SplitLayout {...props}
Area1Content={HomeContent}
Area2Content={SidebarContent} />}/>
</Switch>
</Router>
);
export default App;
This works fine; I can put HomeContent and SidebarContent into a file and export both of them, and they are shown correctly.
However, I want to be able to pass information from one to the other, so, for instance, SidebarContent has a list of names; when I click on a name in the list, I want that person's details to be shown in HomeContent (so in the HomeContent component I can have a state variable called currentPerson, and when a name is clicked in SidebarContent, the value of currentPerson should be changed).
Is there a way to achieve this?
I have several pages with similar layouts, so what I'm hoping is that I can have, eg, a HomeComponent.js file which has HomeContent and SidebarContent, and then another component called, say, SecondComponent.js which has SecondContent and SecondSidebar, so I can just add a new Route to App, something like:
<Route exact path={ROUTES.SECOND}
render={(props) =>
<SplitLayout {...props}
Area1Content={SecondContent}
Area2Content={SecondSidebar} />}/>
so it will render the same layout but with different components. I know I could lift the state up to the top level, but there could potentially be several different component pairs, each needing to pass info, and I think it would get messy to manage all of them at the App level. Is there a better way?
EDIT: I think what I want to do is something like this:
In App.js my route would be something like:
<Route exact path={ROUTES.HOME}
render={(props) =>
<SplitLayout {...props}
PageContent={WrapperComponent} />}/>
Then in the SplitLayout file I'd have something like:
const SplitLayout = (WrapperComponent) => {
return (
<div className="area1">
<WrapperComponent.Area1Content/>
</div>
<div className="area2">
<WrapperComponent.Area2Content/>
</div>
);
}
And WrapperComponent would be something like:
const WrapperComponent = () => {
const [myStateVariable, setMyState] = useState("xyz")
const Area1Content = () => {
return (<div>{myStateVariable}</div>);
}
const Area2Content = () => {
return (<div onClick={setMyState("abc")}>Something else</div>);
}
}
export default WrapperComponent;
Is there a way to do something like that?
You can put the shared state in the parent component, and pass it down as props.

How to map a filter of a string value in an array of strings

I have an app built with React and Express Node, and in it I have 3 separate components. The first component is a gallery, where user selects an image to create a post with an background image. When button is clicked, user is taken to a form. The user will edit the inputs with some text and save the form which has a axios.post request to send the data to mongo db through express route. After saving user clicks view post that takes them to another component with axios.get request displaying image and input data to the user.
I have routes that have a unique http path to show the component that is active. My question is how can I map the routes to dynamically load the name of the image that comes from mongodb collection, instead of manually writing in the paths image name ie: path={"/getinputwaterfall/:id"}, path={"/getinputcross/:id"}, path={"/getinputfreedom/:id"} . I would like to have instead somthing like: path={"/getinput{urlName}/:id"}.
In the mongoDB collection I have a URL and name string array. The URL string is an http path from firebase and the name string are images names.
Is this possible to do?
Bellow is the code and my attempts to do this.
import React, {useState, useEffect} from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import axios from "axios";
import "bootstrap/dist/css/bootstrap.min.css";
import "./index.css";
//gallery imports
import Cross from "./Components/Gallery/posts/Cross";
import Waterfall from "./Components/Gallery/posts/Waterfall";
import Freedom from "./Components/Gallery/posts/Freedom";
import CrossPost from "./Components/Gallery/get/CrossPost";
import WaterfallPost from "./Components/Gallery/get/WaterfallPost";
import FreedomPost from "./Components/Gallery/get/FreedomPost";
function App() {
const [name, setName] = useState([]);
const loadImage = async () => {
try {
let res = await axios.get("http://localhost:5000/geturls");
console.log(res.data)
setName(res.data.map(n=>n.name)); //array of names
} catch (error) {
console.log(error);
}
};
useEffect(() => {
loadImage();
}
,[]);
return (
<div className="App">
<Router>
<Switch>
{/* routes for gallery */}
<Route path={"/waterfall"} component={Waterfall} />
<Route path={"/cross"} component={Cross} />
<Route path={"/freedom"} component={Freedom} />
<Route path={"/getinputwaterfall/:id"} component={WaterfallPost} />
<Route path={"/getinputcross/:id"} component={CrossPost} />
<Route path={"/getinputfreedom/:id"} component={FreedomPost} />
{/* what I tryed to map */}
{name.filter(name => name === `${name}` ).map((urlName) => (
<Route exact path={`/getinputt/${urlName}`} component={CrossPost} />
))}
</Switch>
</Router>
</div>
);
}
export default App;
update: I applied the first option of the answer to my code for those who wish to see the complete solution: Note: I had to remove the '/' in /getinput/${name}/:id to make my code work! Thanks Drew!
const imagePostRoutes = [
{ name: "cross", component: CrossPost },
{ name: "freedom", component: FreedomPost },
{ name: "waterfall", component: WaterfallPost },
];
return (
<div className="App">
<Router>
<Switch>
{imagePostRoutes.map(({ component, name }) => (
<Route
key={name} path={`/getinput${name}/:id`} component={component}
/>
))}
</Switch>
</Router>
</div>
);
}
I was first suggesting to create a "routes" config array that can be mapped.
const imagePostRoutes = [
{ name: "cross", component: CrossPost },
{ name: "freedom", component: FreedomPost },
{ name: "waterfall", component: WaterfallPost },
];
...
{imagePostRoutes.map(({ component, name }) => (
<Route key={name} path={`/getInput/${name}/:id`} component={component} />
))}
The second suggestion is to use a single generic dynamic route where a match parameter could specify the post type and a general post component to render the specific image post component. This is a very stripped down minimal version.
Define the route.
<Route path="/getInput/:imagePostType/:id" component={ImagePost} />
Create a Map of match param to component to render.
const postComponents = {
cross: CrossPost,
freedom: FreedomPost,
waterfall: WaterfallPost,
};
Create a component to read the match params and load the correct post component from the Map.
const ImagePost = () => {
const { id, imagePostType } = useParams();
const Component = postComponents[imagePostType];
if (!Component) {
return "Unsupported Image Post Type";
}
return <Component id={id} />;
}

Read URL parameters

I have a very basic app and I want to read the request parameter values
http://localhost:3000/submission?issueId=1410&score=3
Page:
const Submission = () => {
console.log(this.props.location); // error
return ();
}
export default Submission;
App
const App = () => (
<Router>
<div className='App'>
<Switch>
<Route path="/" exact component={Dashboard} />
<Route path="/submission" component={Submission} />
<Route path="/test" component={Test} />
</Switch>
</div>
</Router>
);
export default App;
Did you setup correctly react-router-dom with the HOC in your Submission component ?
Example :
import { withRouter } from 'react-router-dom'
const Submission = ({ history, location }) => (
<button
type='button'
onClick={() => { history.push('/new-location') }}
>
Click Me!
</button>
)
export default withRouter(Submission)
If you already did that you can access the params like that :
const queryString = require('query-string');
const parsed = queryString.parse(props.location.search);
You can also use new URLSearchParams if you want something native and it works for your needs
const params = new URLSearchParams(props.location.search);
const foo = params.get('foo'); // bar
Be careful, i noticed that you have a functional component and you try to access the props with this.props. It's only for class component.
When you use a functional component you will need the props as a parameter of the function declaration. Then the props should be used within this function without this.
const Submission = (props) => {
console.log(props.location);
return (
<div />
);
};
The location API provides a search property that allows to get the query parameters of a URL. This can be easily done using the URLSearchParams object. For example, if the url of your page http://localhost:3000/submission?issueId=1410&score=3 the code will look like:
const searchParams = location.search // location object provided by react-router-dom
const params = new URLSearchParams(searchParams)
const score = params.get('score') // 3
const issueId = params.get('issueId') // 1410

Resources