Passing both match param and props into react component when using routing - javascript

I have a functional react component and want to render some properties for a selected person in my component.
So I first map a simple list with links to each person
{props.persons.map((person, i) =>{
return(
<li key={i}>
<Link to={`/topics/${person.id}`}>{person.name}</Link>
</li>
)
})}
</ul>
then I make the route
<Route path={`/topics/:id`} render={() => <Topic persons={props.persons} />} />
as of now I just pass in the persons.
However I also want to be able to pass in the id, so I can find the specific person, and render information about that person. I have tried using the matching property, but that seems to prohibit me from passing in props as well.
Any tips to make a workaround?
Maybe it would be possible to just pass in the properties of the selected person?
EDIT:
Here is what i have tried so far.
const Topic = (props) =>{
console.log('props are ', props) //logs array of persons
return(
<div>
<h2>Topic</h2>
<p>I have been rendered</p>
</div>
)
}
const Topic = ({match}) =>{
console.log('match is ', match) //logs match parameter, but now i can't access persons
return(
<div>
<h2>Topic</h2>
<p>I have been rendered</p>
</div>
)
}

When you're using the render prop on a <Route> component, the render function is passed an object with the match, location, and history information.
https://reacttraining.com/react-router/web/api/Route/route-props
<Route
path={`/topics/:id`}
render={({match}) => (
<Topic id={match.params.id} persons={props.persons} />
)}
/>

You no need to pass id as a prop to Topic component of route so instead to get the id of path param you can do following in the component to get the id
In the Topic component
You can get the id using
props.match.params.id

According to https://reacttraining.com/react-router/web/api/Route:
All three render methods will be passed the same three route props
match
location
history
So in the render prop for the route, you should be able to reference the match and pass it along:
<Route
path={`/topics/:id`}
render={({match}) => (
<Topic
persons={props.persons}
id={match.params.id}
/>
)}
/>
This is because the render function is basically a functional component, and the first parameter to a functional component is the props. (However, you don't have to reference the first parameter as props, you could use renderProps, for example, or restructure it into what you need (match) like I did above)
And then in topic:
const Topic = ({id, persons}) =>{
console.log('id is ', id) //logs id parameter
console.log('persons is ', persons) //logs persons parameter
return(
<div>
<h2>Topic</h2>
<p>I have been rendered for id {id}</p>
<code>{JSON.stringify(persons)}</code>
</div>
)
}

You can use useRouteMatch
So in your example:
<Route path={`/topics/:id`} render={() => <Topic persons={props.persons} />} />
Becomes:
<Route exact path={`/topics/:id`} component={() => <Topic persons={props.persons} />}/>
And Topic.js becomes:
import { useRouteMatch } from "react-router-dom"; // Import this
const Topic = (props) =>{
let match =useRouteMatch('/topics/:id').url.split('/'); // Get url and separate with /
match=match[match.length-1]; // The id is at the end of link, so access last array index
console.log('match id is ', match);
console.log('persons is ', persons);
return(
<div>
<h2>Topic</h2>
<p>I have been rendered</p>
</div>
)
}

Related

How can I make an asynchronous setState before following a Link?

I want to be able to 'capture' a selection of a user clicking a link that takes them to another page. I need the users selection to display a detail page of thebselected image.
I'm facing the problem that the browser follows the link before react updated the state "key". The state-Change of key is not passed to the details page. Is there an easy way to fix that without fetch?
export default class IGallery extends React.Component {
constructor(props) {
super(props);
this.state = {
countryList: [],
wallet: "",
key: "",
};
}
handleClick = (_key) => {
console.log("before setState", this.state.key);
this.setState({ key: _key }, () =>
console.log("after setState", this.state.key)
);
};
render() {
return (
<div className={styles.gallery}>
<h1>Gallery</h1>
<ImageList cols={6} rowHeight={320}>
{this.state.countryList.map((data, idx) => (
<ImageListItem key={idx} className={styles.imageItem}>
<Link to="/item" onClick={() => this.handleClick(data.key)}>
<img src={data.image} width={320} />
<ItemDetail id={this.state.key}></ItemDetail>
</Link>
<ImageListItemBar
title={"Braintube"}
subtitle={data.key}
actionIcon={<FavoriteBorder fontSize="large" color="pink" />}
actionPosition="top"
/>
</ImageListItem>
))}
</ImageList>
</div>
);
}
}
I expect that
<ItemDetail id={this.state.key}></ItemDetail>
passes the state value to the child component itemDetail.
Here is my Routing Path from index.js
<Router>
<Header childToParent={childToParent}/>
<Switch>
<Route path="/" exact={true}>
<Home></Home></Route>
<Route path="/project-space">
<ProjectSpace childToParent={wallet}></ProjectSpace>
</Route>
<Route path="/about">
<About></About></Route>
<Route path="/item"><ItemDetail></ItemDetail></Route>
</Switch>
<FooterMain></FooterMain>
</Router>
I think we need to take a step back and understand the React paradigm to answer this question.
In React, state goes only one way and is not retained when a component is unmounted. Right now, we have the following
Router > SomePage > IGallery (State = ....)
and we're trying to redirect to:
Router > ItemPage
As you can see here, moving away to ItemPage will drop state because Router will re-render and SomePage will be unmounted.
Therefore, we have two options:
Pass this item id in the url parameter which will then be handled by the next page
Move the state to the router parent and pass the state's setter + getter down to the page components (unrecommended)
For your situation, option one is more intuitive.

Passing data from parent to child using react.State but State reset to initial state

I am facing an issue while passing the state to the child component, so basically I am getting customer info from child1(Home) and saving in the parent state(App) and it works fine.
And then I am passing the updated state(basketItems) to child2(Basket). But when I click on the Basket button the basket page doesn't show any info in console.log(basketItems) inside the basket page and the chrome browser(console) looks refreshed too.
Any suggestion why it is happening and how can I optimize to pass the data to child2(basket) from main (APP).
update:2
i have tired to simulated the code issue in sand box with the link below, really appreciate for any advise about my code in codesandbox (to make it better) as this is the first time i have used it
codesandbox
Update:1
i have made a small clip on youtube just to understand the issue i am facing
basketItems goes back to initial state
Main (APP)___|
|_Child 1(Home)
|_Child 2 (Basket)
Snippet from Parent main(App) component
function App() {
const [basketItems, setBasketItems] = useState([]);
const addBasketitems = (product, quantity) => {
setBasketItems(prevItems => [...prevItems, { ...product, quantity }])
}
console.log(basketItems) // here i can see the updated basketItems having customer data as expected [{...}]
return (
<Router>
<div className="App">
<header className="header">
<Nav userinfo={userData} userstatus={siginalready} />
</header>
<Switch>
<Route path="/" exact render={(props) => (
<Home {...props} userData={userData} userstatus={siginalready}
addBasketitems={addBasketitems}
/>
)}
/>
<Route path="/basket" exact render={(props) =>
(<Basket {...props} basketItems={basketItems} />
)}
/>
</Switch>
</div>
</Router>
Snippet from the child(basket)
function Basket({basketItems}) {
console.log(basketItems) // here i only get the [] and not the cusotmer data from parent component
return (
<div>
{`${basketItems}`} // here output is blank
</div>
);
}
export default Basket;
Snippet from the child(Home)
... here once the button is pressed it will pass the userselected details to parent
....
<Button name={producNumber} value={quantities[productName]} variant="primary"
onClick={() => {
addBasketitems(eachproduct, quantities[productName])
}}>
Add to Basket
</Button >
Your function works fine, the reason your output in addbasketItem does not change is the when using setState it takes some time to apply the changes and if you use code below you can see the result.
useEffect(()=>{
console.log('basket:',basketItems)
},[basketItems])
Your Basket component only renders once so replace it with this code and see if it works:
function Basket({ basketItems }) {
const [items, setItems] = useState([]);
useEffect(() => {
setItems(basketItems);
}, [basketItems]);
return <div>{`${items}`}</div>;
}
but for passing data between several components, I strongly suggest that you use provided it is much better.

How to recognize where from component is invoked to display proper text

I would like to reuse component which is uploading/adding products to the database,
sometimes product is type of finished product, and sometimes the product is part of unfinished product.
http://localhost:1000/finished-product/add
http://localhost:1000/unfinished-product/add
In my routes I've defined:
<Route exact path="/finished-product/add" component={AddProduct} />
<Route exact path="/unfinished-product/add" component={AddProduct} />
When my component AddProduct is loaded I would like to display proper text for example:
Please add unfinished product : or Please add finished product :
I just want to separate those texts and stuffs like that, so how could I recognize if I'm displaying component
from path="/unfinished-product/add" or from path="/finished-product/add".
Thanks guys
Cheers
Use Route props
As your component is rendered by a Route it gets the following prop :
this.props.location.pathname // => '/finished-product/add' || '/unfinished-product/add'
You can use it to write a condition.
const text = this.props.location.pathname === '/finished-product/add' ? 'Please add finished product' : 'Please add unfinished product';
References:
Route props: https://reacttraining.com/react-router/web/api/Route/route-props
location prop: https://reacttraining.com/react-router/web/api/location
Or use render function with a new prop
Another solution would be using render function in your routes passing a new prop to your component :
<Route exact path="/finished-product/add" render={routeProps => <AddProduct {...routeProps} finished />} />
<Route exact path="/unfinished-product/add" render={routeProps => <AddProduct {...routeProps} />} />
So then you have a finished boolean prop in your AddProduct component.
const text = this.props.finished ? 'Please add finished product' : 'Please add unfinished product';
Reference:
https://reacttraining.com/react-router/web/api/Route/render-func
This can be solved using query params /product/add?status='finished'
while defining routes,
<Route exact path="/product/add" component={AddProduct} />
and in component,
<Link to='/product/add?status="finished"'>Finished Product</Link>
<Link to='/product/add?status="unfinished"'>Unfinished Product</Link>

React: is componentDidUpdate same for 2 different instances of a component?

I am writing a React (ES6, v16) (typescript) application with react-router v4. I am observing a very strange behavior. Here is my render code (very much simplified):
render() {
<Switch>
<Route
path="/foo/add"
render={ props => {
return (
<FormEntry title="add new" />
);
}}
/>
<Route
path="/foo/edit/:id"
render={ props => {
return (
<FormEntry title="edit item" />
);
}}
/>
</Switch>
}
And here is the FormEntry component (simplified):
class FormEntry extends React.Component< { title: string }, any > {
render() {
return (
<div>
{this.props.title}
</div>
);
}
componentDidMount() {
// code goes here
}
componentDidUpdate() {
// code goes here
}
}
Now when, inside the application, I click a link "/foo/add", the handler in the first "Route" component is fired (as expected) and the component "FormEntry" is mounted. The method componentDidMount is rightfully fired.
Now I click the link "foo/edit/1". The handler of the second Route is fired.
This time, inside the "FormEntry" component, the lifecycle method "componentDidMount" is not fired, the method "componentDidUpdate" is called. But this is cleary a different "instance" of the FormEntry which is being mounted. I was expecting the see of the lifecycle methods kicked off...
It looks like there is only one instance of "FormEntry" in my application. So why in the second case (when Route handler for url "foo/edit:id") this instance does not go through the all lifecycle methods??
Is it a breaking change in the v16 version of React? ( I have not observed this behavior in previous versions of react).
Your insight will be very much appreciated
<Switch> check the JSX of the previous matched route and compare it with the new JSX of next route.
If it does match, it will use it and only update changed values without re-mounting components.
Otherwise it will create new react elements and instantiate new components.
Check here: https://github.com/ReactTraining/react-router/pull/4592
A turn around for this is to use key attributes like this:
render() {
<Switch>
<Route
path="/foo/add"
render={ props => {
return (
<FormEntry key="1" title="add new" />
);
}}
/>
<Route
path="/foo/edit/:id"
render={ props => {
return (
<FormEntry key="2" title="edit item" />
);
}}
/>
</Switch>
}

How Destructuring Works In Function Parameters

I tried to search on this but no luck so far. I've looked at destructuring info on the web. I don't understand though this partcular pattern of destructuring.
const App = ({todos, actions}) => (
<div>
<Header addTodo={actions.addTodo} />
<MainSection todos={todos} actions={actions} />
</div>
)
what is {todos, actions} doing here in the function's param definition? What todos and actions is it pulling from?
If you call App like App({todos:10,actions:{addTodo: addTodoFunction}}) in the App function the arguments todos and actions get assigned to 10 and {addTodo: addTodoFunction} respectively. So actions.addTodo becomes addTodoFunction. More at Object Destructuring.
Those are the props that React passes to your component:
<App todos={...} actions={...} />
So you could write your component without destructuring like this:
const App = (props) => (<div>
<Header addTodo={props.actions.addTodo} />
<MainSection todos={props.todos} actions={props.actions} />
</div>)
React passed the props to your component as an object, and destructuring extracts properties from an object, so it is a shortcut!
You could also destructure your props in two steps:
const App = (props) => {
const { todos, actions } = props
return (<div>
<Header addTodo={actions.addTodo} />
<MainSection todos={todos} actions={actions} />
</div>)
}
Notice that in this last case you have to use curly braces and return explicitly because there are multiple statements in your arrow function.

Categories

Resources