I am using react with react router. Some of my components have sub-routes defined so I want to pass them a callback that enables to returning to a specific route/component. I want to avoid passing a string to a specific route (because of the dependency when routing changes happen in the code). So i prefer passing a callback and populating it with the value of match.url.
But this does not work: Instead of passing the value, match.url always refers to the current route.
Parent component (simplified):
export class ParentComponent extends React.Component {
render() {
const { history, match, contentId } = this.props;
return (
<div>
<div>
<div>Block 1</div>
<div>Block 2</div>
<div>Block 3</div>
</div>
{contentId && <MyChildComponent content={contentId} goBack={() => history.push(match.url)} />}
</div>
);
}
}
My child component (simplified):
export class MyChildComponent extends React.PureComponent {
render() {
return (
(
<React.Fragment>
<div role="dialog" onClick={this.props.goBack} />
</React.Fragment>),
);
}
}
My router:
const Routes = () => (
<Router history={createBrowserHistory()}>
<div>
<Route path="/result/:contentId?" component={ParentComponent} />
</div>
</Router>
);
So when I go to /result I see - as expected - all but the child component. When navigating to /result/someId I see the child component but the goBack only refers to the current page instead of the previous one.
constructor(props){
super(props);
this.goBack = this.goBack.bind(this);
}
goBack(){
this.props.history.goBack(); // You're not calling it from history
}
.....
<button onClick={this.goBack}>Go Back</button>
I think that you are using push to navigate to another route. So when you do history.push('/result/someId') you are adding another entry to history stack so goBack will navigate to the previous entry in the stack which is /result. It works as if you were a regular website and clicked a link - you could still go back even if what had changed was some dynamic parameter.
If you don't want to add up to history stack use - history.replace('/result/someId')
See navigating with history.
I figured out my core-problem was that I needed at least one part of the child routes in the parent component. This lead to changing path props also in the parent component when child-routes were changing.
My solution: Store the current location in the constructor of the parent component and pass this as prop to child components to refer back. It works but has the drawback that one can not directly access child component routes because they will not refer back to the right parent path. For my use case this is fine but improvements are welcome.
Parent component
export class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.location = this.props.location.pathname;
}
render() {
return (
{contentId && <MyChildComponent content={contentId} goBack={() =>
history.push(this.location)} />}
)
}
Related
Just started learning web-dev recently so please excuse my perhaps naive question.
I've seen many examples of using <Link> and <Route> to update the page. However, most contain a fixed navigation bar where the links sit, and the components themselves have but static content.
It's not immediately clear to me as to how to correctly update the page if the links/buttons are inside the child component. For example, given a container:
<div id='container'><h1>components should be rendered in here, but only 1 at a time.</h1></div>
and 2 components, C1 and C2:
class C1 extends React.Component {
/*other functions*/
render() {
return(
<div>
<p>Other things</p>
<Button>Click me to Redirect to C2!</Button>
</ div>
)
}
}
class C2 extends React.Component {
/*other functions*/
render() {
return(
<div>
<p>Other things</p>
<a>Click me to Redirect to C1!</a>
<p>Other things</p>
</ div>
)
}
}
Assume these 2 components are to be rendered under the 'container' div, with C1 being the default component.
How should I set up react-router to navigate between C1 & C2, and theoretically for more than two components?
react-router and react-router-dom are two complementary libraries that help you use the Browser history api using Javascript History package under the hood.
To implement what you want you should use BrowserRouter, Switch and Route Components within react-router-dom. BrowserRouter acts as a provider to its children of some utilities and the current location the history is in (current hostname, pathname, queryParams, etc.) while Switch and Route work together by Route being the component that bind a path you want with a React component to render and Switch the component that checks between all its child Route's and Render the first that match the current pathname the history has, you can add a Route without a path prop so Switch falls back to it in case the current history location doesn't match any.
An example Would be something like this
import React from 'react'
import {BrowserRouter, Switch, Route} from 'react-router-dom'
import C1 from './C1'
import C2 from './C2'
export default class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return <div id="container">
<BrowserRouter>
<Switch>
<Route path="/view1" component={C1}/>
<Route path="/view2" component={C2}/>
<Route component={C1} />
</Switch>
</BrowserRouter>
</div>;
}
}
Now, there's multiple ways to navigate through the declared pages. One is to directly use the Link component and pass the pathname you want to go using the 'to' prop.
class C2 extends React.Component {
/*other functions*/
render() {
return(
<div>
<p>Other things</p>
<Link to="/view1">Click me to Redirect to C1!</Link>
<p>Other things</p>
</ div>
)
}
}
Link Actually renders an anchor with the href you gave through the to prop.
Another way to do navigate is to use the history api which is injected as a prop when rendered from a Route. This allows you to add some more logic before actually setting the new location. Here's a simple example
class C2 extends React.Component {
/*other functions*/
redirectToC1(){
// do some extra logic you may want
this.props.history.push("/view1")
}
render() {
return(
<div>
<p>Other things</p>
<a onClick={() => this.redirectToC1()}>Click me to Redirect to C1!</a>
<p>Other things</p>
</ div>
)
}
}
Be sure to dive in on the documentation to see more.
You can call the components as a functions, example :
export default class App extends React.Component {
constructor() {
super();
this.state = {
isActive: true
};
}
C1 = () => {
return <h1>C1</h1>;
};
C2 = () => {
return <h1>C2</h1>;
};
render() {
return <div id="container">{this.state.isActive ? this.C1 : this.C2}</div>;
}
}
with the example above not using any routers. you can see the documentation how to install and use react navigation here: https://reactnavigation.org/docs/hello-react-navigation
You can use Scrollintoview
You can check out this small demo I made for reference demo
This stackoverflow will also be useful
I am using React for my project. I am also using react-router-dom library. However, I am confusing about passing data by react-router-dom.
Here is code for Parent Component:
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
Path: {
pathname: "/child/id/1",
state: {
languageChosen: "English"
}
}
};
this.onChangeLanguage = this.onChangeLanguage.bind(this);
}
onChangeLanguage() {
const { Path } = this.state
Path.state.languageChosen = 'Spanish'
this.setState({Path})
}
render() {
return (
<div>
<NavLink to={this.state.Path}>ToChild</NavLink>
<Route
path={`/child/id/:id`}
render={props => (
<Child {...props} onChangeLanguage={this.onChangeLanguage} />
)}
/>
</div>
);
}
}
It has a state called Path. The Parent Component pass function onChangeLanguage to its Child component.
Here is Child Component:
class Child extends Component {
onChange = () => {
this.props.onChangeLanguage;
};
render() {
const { languageChosen } = this.props.location.state;
return (
<div>
{languageChosen === "English" ? "English" : "Spanish"}
<button onClick={this.onChange}>Change Language</button>
</div>
);
}
}
If I click the Change Lanuguage button in Child Component, the function was called and the state in Parent Component change. The prop LanguageChosen also update in Child component.
However, if I refresh the page, the prop LanguageChosen in Child doesn't update if I press the button. It only works If I don't refresh the page.
How should I fix that. Thanks!
The reason that you get this problem is here
const { languageChosen } = this.props.location.state
languageChose is set from this.props.location,location is set when url change to "/child/id/:id",after refreshing the page,state in Parent element change, but it won't change the location.state.And,I think you can use props in component to solve the problem.
1. set a languageChosen property in route
component={props => (
<Child {...props} onChangeLanguage={this.onChangeLanguage} languageChosen={this.state.Path.state.languageChosen}/>
)}
2. change this in the Child component
const { languageChosen } = this.props.languageChosen;
This code confuses me a bit but, remember when you refresh a page all the javascript gets reloaded, so you hard coded languageChosen to English in your Parent so every time you refresh the page it resets to English. You can either store the value in localStorage and check for its existence on refresh, or add it to your route path child/id/:id/:language
Then if you are using React Router v4, in your child component use this.props.match.params.language and it will render the given language.
class App extends Component {
constructor() {
super();
this.state = {
name: 'React'
};
this.setState=this.setState.bind(this)
}
render() {
return (
<div>
<Hello name={this.state.name} />
<p>
Start editing to see some magic happen :)
</p>
<Child {...this}/>
</div>
);
}
}
child Component
var Child=(self)=>{
return(
<button onClick={()=>{
self .setState({
name:"viswa"
})
}}>Click </button>
)
here I am binding the setState function and send this as props to child component.This will change state of parent from child.Is this proper way?
But this is simple than passing function.If I want change many state change. this will be simple and faster way compare to passing function right ?
The correct way to do this is as simple as yours and does not violate best practices:
class App extends Component {
state = {
name: 'React',
};
render() {
return (
<div>
<Hello name={this.state.name} />
<p>
Start editing to see some magic happen :)
</p>
<Child onClick={() => this.setState({name: 'viswa'})}/>
</div>
);
}
}
const Child=({onClick})=>(
<button onClick={onClick}>Click</button>
);
You shouldn't pass the setState directly, as the setState function will not change the state immediately.
as the documents said:
Think of setState() as a request rather than an immediate command to
update the component. For better perceived performance, React may
delay it, and then update several components in a single pass. React
does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. So you'd better to manage the calling of setState function together as there may have competing of mutating parent's state. It is not recommended to pass this function to another component.
Encapsulating the calling of setState function in one class makes your code stronger.
As #yBrodsky said, you should rather pass down a function which does mutate the state. Here is an example:
class App extends Component {
constructor() {
super();
this.state = {
name: 'React'
};
this.update=this.update.bind(this);
}
update(nextState) {
this.setState(nextState);
}
render() {
return (
<Child updateParent={this.update} />
);
}
}
const Child = ({updateParent}) => (
<button onClick={() => updateParent({name: 'Foo bar'})}>Click</button>
);
You now have full control over the state of the Parent in the child. Just pass the object to be shallowly merged into the parent state. In this example, name in App will change to Foo bar on button click.
i guess in functional components its possible to send your setState to childrens and change parent state in them but not in class base components
So here is my problem. I have an root component that contains navigation and Switch with every component in my page. Navigation is sliding in and out from the left and the way I'm doing this, I'm changing the state in root component, passing prop to and deciding whether or not, should I add class to my nav. The problem is that every component in my app is re-rendering on opening/closing nav. Here is my root component:
class App extends Component {
constructor(props) {
super(props);
this.state = {
navOpen: false
}
}
toggleNav = () => {
this.setState({
navOpen: !this.state.navOpen
})
}
closeNav = (e) => {
if (this.state.navOpen) {
this.setState({navOpen: false})
}
}
render() {
return (
<main>
<Header/>
<Hamburger navOpen={this.state.navOpen} toggleNav={this.toggleNav}/>
<Navigation navOpen={this.state.navOpen} toggleNav={this.toggleNav}/>
<section className="container-fluid content" onClick={this.closeNav}>
<Switch>
<Route path="/add-recipe/:groupId?" component={NewRecipe}/>
<Route path="/recipes/:page?/:sortType?/:size?" component={RecipeList}/>
<Route path="/recipe/:id" component={Recipe}/>
<Route path="/sign/" component={SignForm}/>
<Route path="/user/:id" component={User}/>
</Switch>
</section>
</main>
);
}
componentDidMount() {
this.props.userActions.getUser(this.props.url);
}
}
function mapStateToProps(state) {
return {url: state.url.url, user: state.user.loggedUser}
}
function mapDispatchToProps(dispatch) {
return {
userActions: bindActionCreators(userActions, dispatch)
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App));
Navigation is the only component ( besides hamburger) that cares about his parents state so I have no idea why everything is re-rendering. Does anyone have some ideas?
EDIT:
I've added sCU to my nested components like that:
shouldComponentUpdate(nextProps, nextState) {
// console.log(this.props)
// console.log("next")
// console.log(nextProps)
if (this.props == nextProps) {
return false
}
return true
}
But it didn't change anything. When I open the navigation props for routes remain the same but they still rerender. I tried to move "navOpen" state to navigation and open it from root component via "ref" but every time I call its method I get "Cannot read property 'toggleNav' of null"
You can prevent re-rendering by implementing shouldComponentUpdate on the affected component. Beware though, that rendering does not mean that the DOM is being changed. Rendering in react means that react creates a new copy of its internal node tree and compares it to the DOM to see if and where it needs to update it. So don't worry about re-renders unless your component is expensive to create (i.e. it performs a request to a backend or is heavily animated)
I'm new to react so this is something I don't know. In the app that I
'm working with it has a main component where other components are loaded.
Like this,
render() {
return (
<div className="index">
<HeaderComponent />
<MainHeroComponent />
<AboutComponent />
</div>
);
}
And I want when someone clicks a link in HeaderComponent to show the about component. And hide the MainHeroComponent. How can I do such communication between components in React? Is it possibe?
Thanks
Use React-Router and create routes for this scenario instead of direct communication between components. Sample app structure using react-router
const App = React.createClass({
render() {
return (
<div>
<h1>App</h1>
<HeaderComponent />
</div>
)
}
})
render((
<Router>
<Route path="/" component={App}>
<Route path="hero" component={MainHeroComponent} />
<Route path="about" component={AboutComponent} />
</Route>
</Router>
), document.body)
For more details on router refer: https://github.com/reactjs/react-router/blob/master/docs/guides/RouteConfiguration.md
Aditya's answer is probably a better solution, but if you really want to it your way, you can use state and callbacks.
class Index extends React.Component {
constructor(props) {
super(props);
this.state = {
showHero: true
};
this.toggleShowHero = this.toggleShowHero.bind(this);
}
toggleShowHero() {
this.setState({
showHero: !this.state.showHero
});
}
render() {
return (
<div className="index">
<HeaderComponent onClick={toggleShowHero}/>
{
this.state.showHero ?
<MainHeroComponent /> :
<AboutComponent />
}
</div>
);
}
There are various ways you can achieve this, including React-routers and Redux, but since you're new to React, it'll be good if you get familiar with the basics first. For a start, you have to change the state of the main component to decide which child component to render.
In the main component code snippet you posted, initialize a state in the constructor as follows:
/* in the main component */
constructor(props) {
super(props);
this.state = {
showAbout: true
};
}
Then modify the render function as follows, to pass a reference to your main component, down to your header component:
/* in the main component */
<HeaderComponent mainComponent={this}/>
Then, in HeaderComponent, attach a click event handler to the link on which you want to perform the operation.
/* in HeaderComponent */
<a href="#" ....... onClick={this.showAbout.bind(this)}>Show About</a>
In the same component, define the showAbout function as follows:
/* in HeaderComponent */
showAbout () {
let mainComponent = this.props.mainComponent;
mainComponent.setState({
showAbout: true
)};
}
Finally, back in the render function of the main component:
/* in the main component */
render () {
let mainHeroComponent, aboutComponent;
if (this.state.showAbout) {
aboutComponent = (
<AboutComponent/>
);
} else {
mainHeroComponent = (
<MainHeroComponent/>
);
}
return (
<div className="index">
<HeaderComponent mainComponent={this}/>
{mainHeroComponent}
{aboutComponent}
</div>
);
}
And you're done! Basically, a component gets re-rendered every time its state is changed. So each time you click on the link, the main component's state is changed with a new value of showAbout. This will cause the main component to re-render itself, and, based on the value of showAbout, it will decide whether to render MainHeroComponent or AboutComponent.
But you should make sure you have a similar logic to display MainHeroComponent as well, instead of AboutComponent, just to switch the views.