ReactRouter - props from handlers to header - javascript

I'm trying out React Router. Simplified example to explain my question (at the bottom of the post):
View1
var View1 = React.createClass({
statics: {
headerAction: function() {
this.handleSomething();
}
},
handleSomething: function() {
this.setState(...);
},
render: function () {
return (
<div>
(...)
</div>
);
}
});
Similar class for View2
App
var App = React.createClass({
render: function () {
return (
<div>
<AppHeader />
<RouteHandler/>
</div>
);
}
});
Router
var routes = (
<Route name="app" path="/" handler={App}>
<Route name="view2" handler={View2}/>
<DefaultRoute handler={View1}/>
</Route>
);
Router.run(routes, function (Handler) {
React.render(<Handler/>, document.body);
});
I want AppHeader to be in the App-class (not in the individual views), because the header/toolbar is always visible. If it where a part of the view class, the header would be re-mounted every time the router opened a different view. However, the props for that component can be different based on what route is active. Like a different title, and an icon with a clickable action causing something (state change) in the view.
I have tried to find out a good way to solve this, meaning different props for the AppHeader based on what view class is the active route handler. Since the view class is the component that varies, I thought about having it in the view components (View1/View2 in the example above) even if the props is not going to be used in that component. I have looked at having it as statics in theese component, then extracting them in Router.run, the problem with that is I cannot call event handlers in the same component (like in the View1 example), like clickable links for the header that is to do something in the view.
I tried to have a reflux store, and in componentWillUpdate of the view component, call some action on the store, that is listened to in the app component and sent as props to the header. However, for the first render, that action is called during the render process of the app component, causing the first header props not to be visible because they are not available yet.
Another approach I thought about was having one Reflux store per view (per router handler), and have the props for the header in the individual stores and pass them to the header in the App component. But then I need to switch between the stores to use, based on what route is active. I have not found any way to have individual stores per handler in addition to selecting the store with React Router.
What other option is there for a case like this?

Not sure if this helps but I solved a similar problem using <Link> from react-router.
render(){
let to_obj={pathname:category.url, query:{text:category.text}}
<Link to={to_obj} className="cat-link">
.....
Then at the destination, I update the navbar title via a ReactiveVar with
GlobalState.title.set(this.props.location.query.text)
I haven't tried it but you may even be able to pass function/handlers down.
Just a thought.

Related

Re-render a React Component

I have a profile component with URL: '/profile/id'.
Now, suppose I have two users having IDs: id_A and id_B.
I am at URL: '/profile/id_A' and I have a button which sends me to URL: '/profile/id_B' using:
<Link to={{ pathname: `/profile/id_B` }}></Link>
Now, my component won't re-render because there is no state change for which I cannot retrieve my user-data from the server since all my requests are done on componentDidMount.
So, how do I re-render my component for the above scenario?
UPD 1:
This is my Route code:
<Route path="/profile/:userId" component={Profile} />
UPD 2:
I have 3 components nested:
Profile -> FollowList -> Card
I have my link in my card component and this is the code for routing it to 'profile/id_B'.
<Link to={{ pathname: `/profile/${this.props.user.userId}` }}></Link>
Can this be a problem that the below answers are not working?
Ciao, to re-render a component you can use shouldComponentUpdate(nextProps) function. Basically React triggers this function to ask you if component should be updated (re-rendered) and here you can write the logic you need to re-render the component. Take a look at this guide.
It appears you need to use the Route component, placing your "pages" on them with one of the designed methods. You can read more about it here:
https://reactrouter.com/web/api/Route
The recommended way of doing it is using the children of the Route, which is supposed to mount the page when the url matches, and unmount when they don't. So something like:
<Route path="/to">
<My Page/>
</Route>
The thing is that your component is getting rerendered but as you have your code in componentDidMount and it is called only once when the component is mounted so you are facing the difficulty.
So to fix this problem you need to unmount and mount your component again, this can be done using the inline function inside component prop.
According to react-router-docs
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.
So you need to refactor your code and use
<Route path="/profile/:userId" component={() => (<Profile />)} />
This will mount and unmount on component on each render and you can access the component did mount.
Alternate Solution
You can also use ComponentDidUpdate and check on each update if the id is same or not, if id is not same fetch the data.
componentDidUpdate(prevProps) {
if (prevProps.match.params.userId !== this.props.match.params.userId) {
//call your fetch function here and then set the data in state
setState({
userId: data,
});
}
}

How to dispatch redux actions using react-router v4?

I'm trying to combine react-router v4, react, and redux. Because react-router tracks the URL, I've opted to keep that piece of state out of the redux-model.
But i still need a way to to dispatch a redux action when a route change happens by react-router. Where is the best place to do that?
My first attempt was to put it in the onClick attribute of react-router's Link:
render() {
// link config
const links = this.props.photo.album( album => {
<Link key={album.name}
to=`/album/${album.name}`
onClick={() => this.props.dispatchAction(album.name)} />
})
// route config
return (
<div>
{links}
<Route path={`/album/:albumName`} component={Album}/>
</div>
)
}
The idea is that, when a user clicks a link, the dispatchAction() will update the redux state and then the Album component gets loaded.
The problem is that if a user navigates to the URL directly (eg /album/a1), the action is never dispatched, since the link is technically never clicked.
Because of this I removed the onClick portion of the Link, and moved the dispatchAction to the lifecycle methods of the Album component:
class Album extends Component {
// invoked when user navigates to /album/:albumName directly
componentDidMount() {
this.props.dispatchAction(this.props.match.params.albumName)
}
// invoked whenever the route changes after component mounted
componentWillReceiveProps(nextProps) {
if (this.props.match.params.albumName != nextProps.match.params.albumName) {
this.props.dispatchAction(nextProps.match.params.albumName)
}
....
}
Now whenever the Album component is mounted or its properties changed, it will dispatch the redux-action. Is this the correct approach for combining these libraries?
react-router-redux does this for you by applying a middleware on your store that dispatches actions on route changes, also on the initial route change. It's definitely the cleanest approach.
The only downside is it's still alpha but we have been using it without any issues for a while. It is also part of the react-router additional packages repo for a while.
You could create a custom Route component that dispatches your action in componentDidMount:
class ActionRoute extends React.Component {
componentDidMount() {
const { pathname } = new URL(window.location.href);
const [albumName] = pathname.split('/').slice(-1);
this.props.dispatchAction(albumName);
}
render() {
return <Route {...this.props} />
}
}
This will now dispatch your action whenever the route is loaded. Composability FTW!.
Side note: if you use ActionRoute for parameterized routes, (eg /album/1 and /album/2), you'll need to also dispatch the action in componentDidUpdate as the component isn't unmounted / remounted if you navigate from /album/1 to /album/2.

React Router changing params doesn't fire componentWillRecieveProps

The Problem: Changing the parameters of a <Route /> component does not update the component it is rendering. The route change is shown in the URL bar, but directly rendering {this.props.match.params.id} shows the old :id and not the new one reflected in the URL bar.
Update: I fixed this by moving the <BrowserRouter /> out from the index.js file and into the App.js file. It is no longer the direct child of Provider and is instead the child of the App component. No clue why this makes everything suddenly work.
What I am doing: I have a <Link to="/user/11" /> that goes from user/7 (or any current ID) to a /user/11
The componentWillReceiveProps(newProps) of the component it is rendering is not fired.(This component is connected using react-redux if that helps any. I tried applying withRouter around the connection and that did not help)
If I manually refresh the page in chrome (using CTRL-R or the refresh button) the page shows the new data, rendering the "new" param.
TLDR: Switching from /user/7 to /user/11 does not fire that componentWillRecieveProps function and therefore leaving the component displaying the old state
Question: What am I doing incorrectly here that causes componentWillReceiveProps to not fire.
I am using react-router v4 and the latest create-react-app
This is my CWRP function:
componentWillReceiveProps(newProps) {
console.log("getProps")
this.props.getUser(newProps.match.params.id)
if (newProps.match.params.id == newProps.currentUser.id) {
this.setState({ user: "currentUser" })
} else {
this.setState({ user: "selectedUser" })
}
}
This is the full code of my component: https://gist.github.com/Connorelsea/c5c14e7c54994292bef2852475fc6b43
I was following the solution here and it did not seem to work for me. Component does not remount when route parameters change
You'll need to use React Router Redux

Store fetched data to avoid fetching it with each reload of the component

I have a React component that is supposed to be used in different views. This component is Select element with pretty long list of options, and I load this list from db via ajax call.
For all views used the same Select with the same list of options.
My problem is that I don't understand how to coerce this Select to load this list only once, and reuse it for other renderings.
It looks somewhat like this now (simplistically, obviously):
var SelectField = React.createClass({
loadListFromServer: function () {...}
getInitialState()
{
return {
options: [],
};
},
componentDidMount() {
this.setState({
options: this.loadListFromServer()
});
},
render: function () {
return (
<div>
<Select options={this.state.options} />
</div>
);
}
})
;
var Index = React.createClass({
render: function () {
return (
<SelectField />
);
}
});
var Content = React.createClass({
render: function () {
return (
<SelectField />
);
}
});
ReactDOM.render(
<Router history={createBrowserHistory()}>
<Route path="/" component={Index}/>
<Route path="path" component={Content}/>
</Router>,
document.getElementById("container")
)
What I tried to do: make both options and loadListFromServer() global and call the loadListFromServer() from window.init. Then Index renders with empty options as it is being filled when everything is already mounted.
So what is general approach to achieve it? Thanks, and I am sorry if my question is stupid - I've just started this topic.
When you say you only want to load the <Select> component once, I assume you mean you only want to load its data once.
You might try a flux architecture that separates components from actions.
The root of the problem in your example seems to be the tight coupling between the <Select> component and the state that it presents (the list of options). Every time the component is used, it must create its state or reuse the state from a different instance of <Select>. But in the latter case we would need somewhere to store the state between different instances of the component.
Have you looked into redux? It decouples the state from components.

How can I prevent React from unmounting/remounting a component?

I am using react-router and react-redux. I have two routes like this:
<Route path='/edit' component={ EditNew } />
<Route path='/edit/:id' component={ EditDraft } />
where EditNew and EditDraft are data-providing containers that wrap an Editor component using the react-redux connect function:
const EditNew = connect(state => ({}))(React.createClass({
render() {
return <Editor />;
}
}));
and
const EditDraft = connect(state => ({ drafts: state.drafts }))(React.createClass({
render() {
const { params, drafts } = this.props;
const draft = findDraft(params.id, drafts);
return <Editor draft={ draft } />;
}
}));
Now, Editor is rigged up in such a way that when you begin typing into a blank Editor, it triggers a history.replaceState() from /edit to /edit/:id with a ranomly generated ID. When this happens, I get the following sequence of events:
EditorNew unmounts
Editor unmounts
EditorDraft renders and mounts
Editor renders and mounts
When I coded these two containers, I thought that the Editor component contained in both of them would be reconciled without unmounting and remounting. This is problematic for me for several reasons besides the extra unnecessary work, chief among which are that the editor ends up losing focus and proper cursor range after the unmount and remount.
To no avail I have tried specifying key for the Editor component to hint to the reconciliation system that it's the same component, and I've tried shouldComponentUpdate, but that doesn't get called, which makes sense given what React is doing.
Apart from combining the two containers into one container with more complicated render() logic, is there anything I can do to prevent the Editor component from unmounting/remounting during the history transition?
React’s Reconciliation Algorithm says that if the element has a different type (in this case, EditNew and EditDraft), then React will “tear down the old tree and build the new tree from scratch.”
To prevent this, you need to use the same component for both routes.
You can use shouldComponentUpdate and, if the route has changed from /edit to /edit/:id (you can check this getting the router info from the state connected to your component) return false, so it won't refresh the component.
Chances are that this isn't possible with react-router <= v3.
With react-router v4, this should be possible now: https://github.com/ReactTraining/react-router/issues/4578

Categories

Resources