React Redux Url Change Events / Architecture - reactjs

I'm building an application with React. If someone wants to search the following happens:
State is updated: this.props.Address.selectedAddress
Click on a search button the search event is trigger AND my URL is changed:
this.props.handleSearch(address);
let city = format_city(this.props.Address.selectedAddress);
browserHistory.push('/searchfor/'+city);
That works fine. Now I would like that a search event is also triggered if someone puts a new url in the application. What I did so far:
class ResultsList extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.unlisten = browserHistory.listen( (location) => {
console.log('route changes');
if (this.selectedAdressNOTMatchUrl()){
this.handelAdressSearch()
}
});
}
componentWillUnmount() {
this.unlisten();
}
Now the problem that the click event changes the url and the result list component thinks it have to handle this event: triggering search and updateting selectedAddress.
I really think it is a bad design how I have done it. Of course I could add a timestamp or something like this to figure out if browserHistory.push('/searchfor/'+city); happend right before the url change event is fired, but that is a bit ugly.
Every idea is appreciated!
Here is my setup:
export const store = createStore(
reducers,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
compose(
applyMiddleware(
thunkMiddleware,
promiseMiddleware()),
autoRehydrate()
)
);
const persistor = persistStore(store, {storage: localForage});
render(
<Provider store={store} persistor={persistor}>
<Router history={browserHistory}>
<Route path="/" component={Home}/>
<Route path="/searchfor/:city" component={Suche}/>
</Router>
</Provider>,
PS: One more remark
I tried react-router-redux. It saves the url state. BUT it doese not solve the issue. If for example I enter a new url. Then the Event LOCATION_CHANGE is fired. BUT short after that, my persist/REHYDRATE event is fired. So that the url changes back. That makes the behavior even worse...
PPS: To make it more clear. Everything works fine. I want to add one more feature. If the users enters a new url himself, then the application should handle this the same way if he clicked on the search button. BUT it seems a bid of hard, since in that case in my application the rehydrate event is fired...

Not sure how redux-persist ("rehydrating") works so maybe I'm missing something crucial.. But I'm thinking it could be possible to solve your issue through connecting your ResultsList component to Redux so that it passes the react-router url-parameter to it, something like this
// ownProps is be the props passed down from the Route, including path params
const mapStateToProps = (state, ownProps) => {
return {
searchTerm: ownProps.city, // the url param
results: state.searchResults // after a manual url entry this will be undefined
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleSearch: searchTerm => dispatch(searchAction(searchTerm))
};
};
(That kind of code should be possible according to these docs here)
Then, inside the ResultsList, you could initiate the search.
componentDidMount() {
const {searchTerm, handleSearch} this.props;
handleSearch(searchTerm);
}
}
render() {
const {results} = this.props;
return !results ?
<p>Loading..</p> :
<div>
{ results.map(renderResultItem) }
</div>
}
That way the component would not have to care if the URL-param came from a manually entered url, or from a programmatically pushed URL from the search page.

Related

Is it possible to avoid remounting components if query params or route params change?

I have some page filter like sortBy, groupId etc which needs be in the URL as the user may come to the filtered view from other pages. I tried keeping it in both router params and query params but the issue is that whenever these params change in the url the entire component is being re-mounted. Is there any way to avoid this or a better way to handle filters on a listing page.
// users main container
<Switch>
<Route
path={`${this.props.match.url}/users/:sort_by/:group_id`}
component={UsersListContainer}
/>
</Switch>
usersListContainer
import React, { Component } from 'react';
import { connect } from 'react-redux';
class UsersListConainer extends Component {
componentDidMount() {
this.props.getStats();
this.props.getUsers();
}
.......
........
render() {
const { usersData, userStats } = this.props;
return (
<>
<Stats data={userStats} />
<Table data={usersData} />
</>
);
}
}
const mapStateToProps = (state) => {
const { usersData, userStats } = state.users;
return {
usersData,
userStats
};
};
export default connect(mapStateToProps, null)(UsersListConainer);
Thanks in advance!
The behavior is expected since your path changes anytime you sort or group your users list differently. However, you don't really want unique pages for this.
I would just use URL query params for the sorting and grouping. That won't require a component rerender. So change your your route to be:
<Route
path={`${this.props.match.url}/users`}
component={UsersListContainer}
/>
This will only cause a rerender whenever the props.match.url changes.
As for the grouping and sorting, just use the URL API. These are parameters that shouldn't be part of your route:
const url = new URL(window.location.href);
To read and query the correct data, you can then parse these when your component mounts before data fetching:
const url = new URL(window.location.href);
const sortBy = url.searchParams.get('sortBy');
const groupBy = url.searchParams.get('groupBy');
If you want to change the ordering or grouping, simply write the new query parameters to the URL so that they persist after refresh. You can do this programatically (in an onClick handler, for example):
url.searchParams.set('sortBy', 'foo');
url.searchParams.append('groupBy', 'bar');
window.location.search = url;
// retrigger fetch here

Common DRY principle violation React Native/Redux

I seem to be having a reoccurring issue that I'm hoping there is a design solution out there that I am not aware of.
I'm running into the situation where I need to dispatch the exact same things from two different components. Normally I would set this to a single function and call the function in both of the components. The problem being that if I put this function (that requires props.dispatch) in some other file, that file won't have access to props.dispatch.
ex.
class FeedScreen extends Component {
.
.
.
componentWillReceiveProps(nextProps) {
let {appbase, navigation, auth, dispatch} = this.props
//This is to refresh the app if it has been inactive for more
// than the predefined amount of time
if(nextProps.appbase.refreshState !== appbase.refreshState) {
const navigateAction = NavigationActions.navigate({
routeName: 'Loading',
});
navigation.dispatch(navigateAction);
}
.
.
.
}
const mapStateToProps = (state) => ({
info: state.info,
auth: state.auth,
appbase: state.appbase
})
export default connect(mapStateToProps)(FeedScreen)
class AboutScreen extends Component {
componentWillReceiveProps(nextProps) {
const {appbase, navigation} = this.props
//This is to refresh the app if it has been inactive for more
// than the predefined amount of time
if(nextProps.appbase.refreshState !== appbase.refreshState) {
const navigateAction = NavigationActions.navigate({
routeName: 'Loading',
});
navigation.dispatch(navigateAction);
}
}
}
const mapStateToProps = (state) => ({
info: state.info,
auth: state.auth,
appbase: state.appbase
})
export default connect(mapStateToProps)(AboutScreen)
See the similar "const navigateAction" blocks of code? what is the best way to pull that logic out of the component and put it in one centralized place.
p.s. this is just one example of this kind of duplication, there are other situations that similar to this.
I think the most natural way to remove duplication here (with a react pattern) is to use or Higher Order Component, or HOC. A HOC is a function which takes a react component as a parameter and returns a new react component, wrapping the original component with some additional logic.
For your case it would look something like:
const loadingAwareHOC = WrappedComponent => class extends Component {
componentWillReceiveProps() {
// your logic
}
render() {
return <WrappedComponent {...this.props} />;
}
}
const LoadingAwareAboutScreen = loadingAwareHOC(AboutScreen);
Full article explaining in much more detail:
https://medium.com/#bosung90/use-higher-order-component-in-react-native-df44e634e860
Your HOCs will become the connected components in this case, and pass down the props from the redux state into the wrapped component.
btw: componentWillReceiveProps is deprecated. The docs tell you how to remedy.

Can someone explain what is the best way to share an URL that can keep track of the Redux state?

I'm having hard times understanding how I can keep the current state when sharing an URL.
I have an app made with React and Redux.
When I click on a button I'm dispatching an action to update the store and I'm changing the route at the same time.
The Redux store gets updated and the component gets rendered with data from the Redux store.
If I want to share the URL to a person that has the same app, that person does not have the store updated so it wont render anything.
I tried to summarize the code:
on route: /
<Home>
<BookList data={featured} />
<BookList data={top} />
</Home>
on route: /record/:recid
<BookDetails>
<BookInfo />
<BookList data={suggested}/>
</BookDetails>
BookList is a reusable component:
<BookList>
<BookItem onCLick={goToDetailsPage}/>
<BookItem />
<BookItem />
<BookItem />
</BookList>
How can I solve this issue?
Thank you for all the help you can provide!
In order to load the correct state to your model view (book), you'll need to have access to the necessary variables. Two simple ways to achieve this is through a url param ?recid=12 or using react-router's match params, which you're doing here. You have access to the :recid through the route match.params.recid.
React docs recommend fetching data in componentDidMount(), but there's nothing wrong with fetching new data in componentDidUpdate() when your path updates. Remember to keep track of your fetching state, so you don't make multiple api calls. A watered down version could look something like this:
class BookModel extends Component {
constructor {...}
componentDidMount() {
const { dispatch, match } = this.props;
const { recid } = match.params;
dispatch(someFetchAction("book", recid));
}
componentDidUpdate(prevProps) {
const { dispatch, fetching, match } = this.props;
const oldBook = prevProps.match.params.recid;
const newBook = match.params.recid;
const didUpdate = oldBook !== newBook;
//fetch new record if not already fetching and record id has changed
!fetching && didUpdate && dispatch(someFetchAction("book", newBook));
}
render() {...}
}
export default connect((store, props) => ({
bookModels: store.bookModels,
fetching: store.fetching
}))(BookModel);
If you're experiencing issues with updates not happening, you may want to add withRouter, but I don't believe it would be necessary in this case.

Redux persist rehydrate previous auth state too late

I have setup routes that are meant to be authenticated to redirect user to login page if unauthenticated. I have also setup redux-persist to auto dehydrate my auth state so user remains login on refresh. The problem is this rehydration is too late and user is already redirected to login page
The 1st location change is to an authenticated route, the 2nd is to login. Notice rehydrate comes after these. Ideally it should be right after ##INIT at least?
The persistStore function which is used to make your store persistent have a third param callback which is invoked after store rehydration is done. You have to start your app with some sort of preloader, which waits for rehydration to happen and renders your full application only after it finishes.
redux-persist docs even have a recipe for this scenario. In your case all the react-router stuff should be rendered inside initial loader as well:
export default class Preloader extends Component {
constructor() {
super()
this.state = { rehydrated: false }
}
componentWillMount(){
persistStore(this.props.store, {}, () => {
this.setState({ rehydrated: true });
})
}
render() {
if(!this.state.rehydrated){
return <Loader />;
}
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>
);
}
}
const store = ...; // creating the store but not calling persistStore yet
ReactDOM.render(<Preloader store={store} />, ... );
When user refresh the page, The first LOCATION_CHANGE will fire because you sync history with store. However, it only syncs the history and does not redirect user to anywhere yet.
The only thing matters is the second LOCATION_CHANGE, which should occur after persistStore(). Good news is persistStore() has a call back.
const store = ...
// persist store
persistStore(store, options, () => {
const states = store.getState();
// if authenticated, do nothing,
// if not, redirect to login page
if (!states.auth.isAuthenticated) {
// redirect to login page
}
});
render(...);
Hope this can help to solve your problem.

Prevent react-native-router-flux from rendering all components

I'm using React-Native-Router-Flux for routing my app. The issue is that it seems like when a redux state changes, ALL the components under the Router gets rerendered, not just the "current" component.
So lets say I have 2 components under the Router: Register and Login and both share the same authenticationReducer. Whenever an authentication event (such as user registration or signin) fails, I want to display error Alerts.
The problem is that when an error is fired from one of the components, two Alerts show up at the same time, one from each component. I assumed when I am currently on the Register scene, only the error alert would show from the Register component.
However, it seems like both components rerender whenever the redux state changes, and I see 2 alerts (In the below example, both 'Error from REGISTER' and 'Error from SIGNIN').
Here are the components:
main.ios.js
export default class App extends Component {
render() {
return (
<Provider store={store}>
<Router>
<Scene key='root'>
<Scene key='register' component={Register} type='replace'>
<Scene key='signin' component={SignIn} type='replace'>
</Scene>
</Router>
</Provider>
);
}
}
Register.js
class Register extends Component {
render() {
const { loading, error } = this.props;
if (!loading && error) {
Alert.alert('Error from REGISTER');
}
return <View>...</View>;
}
}
const mapStateToProps = (state) => {
return {
loading: state.get("authenticationReducer").get("loading"),
error: state.get("authenticationReducer").get("error"),
};
};
export default connect(mapStateToProps)(Register);
SignIn.js
class SignIn extends Component {
render() {
const { loading, error } = this.props;
if (!loading && error) {
Alert.alert('Error from SIGNIN');
}
return <View>...</View>;
}
}
const mapStateToProps = (state) => {
return {
loading: state.get("authenticationReducer").get("loading"),
error: state.get("authenticationReducer").get("error"),
};
};
export default connect(mapStateToProps)(SignIn);
How do I change this so that only the REGISTER error message shows when I am currently on the Register Scene, and vice versa?
Thanks
Because of the way react-native-router-flux works, all previous pages are still "open" and mounted. I am not totally sure if this solution will work, because of this weird quirk.
Pretty strict (and easy) rule to follow with React: No side-effects in render. Right now you are actually doing a side-effect there, namely, the Alert.alert(). Render can be called once, twice, whatever many times before actually rendering. This will, now, cause the alert to come up multiple times as well!
Try putting it in a componentDidUpdate, and compare it to the previous props to make sure it only happens once:
componentDidUpdate(prevProps) {
if (this.props.error && this.props.error !== prevProps.error) {
// Your alert code
}
}
I am not totally convinced that this will actually work, as the component will still update because it is kept in memory by react-native-router-flux, but it will at least have less quirks.
I solved this by creating an ErrorContainer to watch for errors and connected it to a component that uses react-native-simple-modal to render a single error modal throughout the app.
This approach is nice because you only need error logic and components defined once. The react-native-simple-modal component is awesomely simple to use too. I have an errors store that's an array that I can push errors to from anywhere. In the containers mapStateToProps I just grab the first error in the array (FIFO), so multiple error modals just "stack up", as you close one another will open if present.
container:
const mapStateToProps = (
state ) => {
return {
error: state.errors.length > 0 ? state.errors[0] : false
};
};
reducer:
export default function errors (state = [], action) {
switch (action.type) {
case actionTypes.ERRORS.PUSH:
return state.concat({
type: action.errorType,
message: action.message,
});
case actionTypes.ERRORS.POP:
return state.slice(1);
case actionTypes.ERRORS.FLUSH:
return [];
default:
return state;
}
}

Resources