React-router Pass Data - javascript

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.

Related

React child component not updating parents state, so second child is not noticing the changes

Curently I have this structure.
Child1 <---- Parent ----> Child2
Parent component has this state
this.state = {
authenticationChecked: true,
isAuthenticated: true,
sideBarRetracted: false
}
this.retract = this.retract.bind(this);
this.unretract = this.unretract.bind(this);
and the following functions
retract() {
this.setState({
sideBarRetracted: true
}, () => {
console.log("retracted")
console.log(this.state.sideBarRetracted) //I can see this changing to true, but the child wont take it into effect, even tho if i hardcode it , it will
});
}
unretract() {
this.setState({
sideBarRetracted: false
})
console.log("unretracted")
}
Which is pased to both childs
<SideBar retract={this.retract} unretract={this.unretract} retracted={this.state.sideBarRetracted} {...this.props}/>
<PrivateRoute authed={this.state.isAuthenticated} path="/skadi" render={(props) => <Skadi retracted={this.state.sideBarRetracted} {...props}/>} />
Now my question is. I supposedly should receive that as a prop in a child component, for example lets say in the sidebar component which is a child. Whose constructor is this, and im supposed to receive the prop there and assign it to the state
constructor(props) {
super(props);
this.state = {
retracted: this.props.retracted
}
}
It works properly so far, but now I have the following issue. This same component has a function onClick() which will change that specific state
<FontAwesomeIcon className="registerIcon" icon={faArrowAltCircleLeft} onClick={this.props.retract} />
<FontAwesomeIcon className="expandicon-retracted" icon={faArrowAltCircleRight} onClick={this.props.unretract} />
But point is, this is not working, the state is not being updated in the parent component. I can change it manually (hardcoding) and I see the child component reacting to the change, but i cant make it react with the onclick() update
At the same time, I have my Child2 component, who has been passed the parent state as well. Will its state update if I update the parent component using CHILD1 ?
I have seen that i should use something like this in the child2 , but well it wont work so far (mainly because parent component isnt updated afaik)
constructor(props) {
super(props)
this.state = {
sideretracted: false
}
console.log("props in skadi")
}
componentWillReceiveProps(nextProps) {
this.setState({ sideretracted: nextProps.sideBarRetracted});
}
You shouldn't set the child state from the props received from the parent component.
Since you are updating the state of child component from the methods bound to the child components it isn't reflected in the parent component.
One thing you could do is pass a method from the parent to update the state of the parent component and the child state gets updated by itself since it will be passed the new state variable from parent as a prop.
You could have the toggleRetract method defined in the parent to set the parent state and pass it as a prop to the child component which would trigger it and set the parent state for you which will be passed down to the children components as props (like how you are passing the state props).
For example
function Toggle() {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(o => !o)
return <Switch on={on} onToggle={toggle} />
}
function Switch({on, onToggle}) {
return (
<div>
<div>The button is {on ? 'on' : 'off'}</div>
<button onClick={onToggle}>Toggle</button>
</div>
)
}
Here Toggle is the parent component and Switch is the child component and the parent component passes the on state as well as onToggle method which the child component uses to display and update the state respectively.
More info here

React router pass callback with current route to child component

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)} />}
)
}

Making react children NOT to refresh when parents state changes

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)

How to pass a wrapped component into React Router without it constantly remounting

We're in the process of upgrading our React App, and after many of hours of pain have realised that passing wrapped components into React Router (V4 and maybe others) causes the component to "remount" every time a new prop is passed in.
Here's the wrapped component...
export default function preload(WrappedComponent, props) {
class Preload extends React.Component {
componentWillMount() {
getDataForComponent(props);
}
render() {
return <WrappedComponent {...props} />;
}
}
return Preload;
}
And here's how we're using it...
const FlagsApp = (props) => {
return (
<Route path="/report/:reportId/flag/:id/edit" component{preload(FlagForm, props)} />
);
};
Anytime we're dispatching an action and then receiving a update, the component remounts, causing lots of problems. According to this thread on github, components will remount if:
you call withRouter(..) during rendering which would create a new component class each time
you pass a new function to Route.component each render, e.g. using anonymous function
{...}} />, which would create a new component as well
If I pass the FlagForm component in directly the problem is fixed, but then I can't take advantage of the preload function.
So, how can I achieve the same outcome, but without the component remounting?
Thanks in advance for any help!
The reason Route is mounting a new component on every update is that it's been assigned a new class each time via preload.
Indeed, each call to preload always returns a distinct anonymous class, even
when called with the same arguments:
console.log( preload(FlagForm,props) != preload(FlagForm,props) ) // true
So, since the issue is that preload being called within the FlagsApp component's render method, start by moving it outside of that scope:
const PreloadedFlagForm = preload(FlagForm, props) //moved out
const FlagsApp = (props) => {
return (
<Route path="/report/:reportId/flag/:id/edit"
component={PreloadedFlagForm} /> //assign component directly
);
};
This way the component for Route won't change between updates.
Now about that lingering props argument for preload: this is actually an anti-pattern. The proper way to pass in props just the standard way you would for any component:
const PreloadedFlagForm = preload(FlagForm) //drop the props arg
const FlagsApp = (props) => {
return (
<Route path="/report/:reportId/flag/:id/edit"
component={<PreloadedFlagForm {...props} />} //spread it in here instead
/>
);
};
And so the code for preload becomes:
export default function preload(WrappedComponent) {
class Preload extends React.Component {
componentWillMount() {
getDataForComponent(this.props);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
return Preload;
}
Hope that helps!
If like me you didn't read the instructions, the answer lies in the render prop of the <Route> component
https://reacttraining.com/react-router/web/api/Route/render-func
render: func
This allows for convenient inline rendering and wrapping without the undesired remounting explained above.
So, instead of passing the wrapper function into the component prop, you must use the render prop. However, you can't pass in a wrapped component like I did above. I still don't completely understand what's going on, but to ensure params are passed down correctly, this was my solution.
My Preload wrapper function is now a React component that renders a Route...
export default class PreloadRoute extends React.Component {
static propTypes = {
preload: PropTypes.func.isRequired,
data: PropTypes.shape().isRequired,
location: PropTypes.shape({
pathname: PropTypes.string.isRequired,
}),
}
componentWillMount() {
this.props.preload(this.props.data);
}
componentWillReceiveProps({ location = {}, preload, data }) {
const { location: prevLocation = {} } = this.props;
if (prevLocation.pathname !== location.pathname) {
preload(data);
}
}
render() {
return (
<Route {...this.props} />
);
}
}
And then I use it like so...
const FlagsApp = (props) => {
return (
<Switch>
<PreloadRoute exact path="/report/:reportId/flag/new" preload={showNewFlagForm} data={props} render={() => <FlagForm />} />
<PreloadRoute exact path="/report/:reportId/flag/:id" preload={showFlag} data={props} render={() => <ViewFlag />} />
<PreloadRoute path="/report/:reportId/flag/:id/edit" preload={showEditFlagForm} data={props} render={() => <FlagForm />} />
</Switch>
);
};
The reason I'm calling this.props.preload both in componentWillMount and componentWillReceiveProps is because I then had the opposite issue of the PreloadRoute component not remounting when navigating, so this solves that.
Hopefully this save lots of people lots of time, as I've literally spent days getting this working just right. That's the cost of being bleeding edge I guess!

Communicating between React components

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.

Categories

Resources