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>
}
Related
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.
At the moment, I have the following routes in my App.js file:
<Switch>
<Route exact path="/new-job"
render={(props) => <NewJob jobName={jobName} setMenuSelection={handleMenuSelection} />}
/>
<Route exact path="/past-jobs"
render={(props) => <PastJobs setMenuSelection={handleMenuSelection} />}
/>
</Switch>
Now within my PastJobs component, I have the following button with onClick process:
<Button
onClick={() => {
setConfirmDialog({
isOpen: true,
title: `Copy Job ${item.id}?`,
onConfirm: () => { onCopy(item.job_info) }
})
}}
>
Copy
</Button>
that calls the following function:
const onCopy = (job_info) => {
setConfirmDialog({
...confirmDialog,
isOpen: false
})
history.push({
pathname: '/new-job',
state: { detail: job_info }
})
}
Within my <NewJob /> component, I have now setup the following as I thought I could access the state.detail but unfortunately it's null, i.e.:
import { useLocation } from 'react-router-dom';
function NewJob( { jobName, setMenuSelection } ) {
const { state } = useLocation();
if (typeof state !== 'undefined') {
const myVal = state.detail
console.log("myVal", myVal )
}
}
The issue that I am having and unsure how to approach is that within my onCopy function that is called from button onClick, how do I call the the <NewJob /> component whose path is exact path="/new-job" in App.js above and pass in the prop job_info ?
Direct calls to components actually does not exist. But what you are looking can be achieved in different ways.
Using state machine with event bus (redux, redux-saga)
Render props https://reactjs.org/docs/render-props.html
Bunch of callbacks drilled via props (HOC's)
Ref's https://reactjs.org/docs/refs-and-the-dom.html
I suggest to read more about them to actually understand if it matches your use case. Anyhow it is great experience to develop your skills also!
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.
To keep it simple, the detail page fetches data on mount based on the movie ID in the URL, this coming from path='movie/:id' in the Route.
It's child is called Recommended, which shows you recommended movies based again on the current URL.
class MovieDetailPage extends React.Component {
// Fetch movies and cast based on the ID in the url
componentDidMount() {
this.props.getMovieDetails(this.props.match.params.id)
this.props.getMovieCast(this.props.match.params.id)
}
render() {
<div>
Movies here
</div>
<Recommended id={this.props.match.params.id}/>
}
}
The Recommended component fetches data based on the current movie as well and generates another tag pointing to another movie.
class Recommended extends React.Component {
componentDidMount() {
this.props.getRecommended(this.props.id)
}
render() {
return (
<>
<Category title={'Recommended'}></Category>
<div className="movies">
{
this.props.recommended.map((movie) => {
return (
<Link key={movie.id} to={`movie/${movie.id}`} className="movies__item">
<img
key={movie.id}
src={`https://image.tmdb.org/t/p/w342${movie.poster_path}`}
className="movies__item-img"
alt={`A poster of ${movie.title}`}
>
</img>
</Link>
)
})
}
</div>
</>
)
}
}
Now how can I trigger another render of the parent component when clicking the Link generated in the Recommended component? The URL is changing but this won't trigger a render like I intent to do.
UPDATE:
<Route
path="/movie/:id"
render={(props) => (
<MovieDetailPage key={props.match.params.id}
{...props}
)}
/>
I passed in a unique key this time that triggered the re-render of the page. I tried this before but I might've screwed up the syntax.
This post got me in the right direction: Force remount component when click on the same react router Link multiple times
Add a key to the page
If you change route but your page is not getting its "mount" data then you should add a key to the page. This will cause your page to rerender and mount with the new id and get the data again.
You can read more about react keys here
A key tells react that this is a particular component, this is why you see them in on lists. By changing the key on your page you tell react that this is a new instantiation of the component and has changed. This will cause a remount.
Class component example
class MyPage extends React.Component {
componentDidMound() {
// this will fire each time the key changes since it triggers a mount
}
render() {
return (
<div key={props.pageId}>
{/* component stuff */}
</div>
)
}
}
Functional component example
const MyPage = (props) => {
React.useEffect(() => {
// this will fire each time the key changes
}, []);
return (
<div key={props.pageId}>
{/* component stuff */}
</div>
)
}
You can add another React lifecycle method that triggers on receiving new props (UNSAFE_componentWillReceiveProps, componentDidUpdate, getDerivedStateFromProps) in your Recommended component like this:
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.id !== this.props.id) {
nextProps.getRecommended(nextProps.id);
};
}
You can also add key to your component (which forces it to re-render completely if key changed) like this:
<Recommended key={this.props.match.params.id} id={this.props.match.params.id}/>
You can also use React Hooks to handle this more easily with useEffect:
const Recommended = (props) => {
const { id, getRecommended, recommended } = props;
useEffect(() => {
id && getRecommended(id);
}, [id]);
return (
<>
<Category title={'Recommended'}></Category>
<div className="movies">
{recommended.map((movie) => {
return (
<Link key={movie.id} to={`movie/${movie.id}`} className="movies__item">
<img
key={movie.id}
src={`https://image.tmdb.org/t/p/w342${movie.poster_path}`}
className="movies__item-img"
alt={`A poster of ${movie.title}`}
></img>
</Link>
);
})}
</div>
</>
);
};
Note: adding key to component and complete its re-render is not best practice and you should be using Component's lifecycles to avoid it if possible
I have a parent component that should render another component when the URL is matches a certain path:
const View: React.SFC<Props> = ({
....
}) => {
return (
<div>
....
<Route path={jobPath} component={JobPanel} />} />
</div>
);
};
JobPanel.tsx will render if jobPath === /careers/:id which all works.
JobPanel.tsx has a link that will currently go back with this.props.history.push(/careers)
<BackLink
to="/company/careers"
onClick={(e: any) => { handleClose(); }}
>
<StyledChevron orientation={Orientation.Left} />
Go Back
</BackLink>
or
<BackLink
onClick={(e: any) => { this.props.history.push('/careers/); handleClose(); }}
>
<StyledChevron orientation={Orientation.Left} />
Go Back
</BackLink>
The problem is that JobPanel is supposed to have a transition in and out of the page with this Component:
class JobPanel extends Component {
render() {
const { isOpen, handleClose, job } = this.props;
return (
<StyledFlyout
active={isOpen}
Where isOpen is a boolean value in redux store.
While rendering JobPanel all works, I believe react-router is causing the page to re-render whenever the URL is changed. I'm not entirely sure on how to achieve no re-rendering.
Use the render prop instead of component in the Route. eg:
<Route path={jobPath} render={({ history }) => (
<JobPanel {...routeProps} />
)} />
From https://reacttraining.com/react-router/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).
For more details on using render see https://reacttraining.com/react-router/web/api/Route/render-func for more details.