react-router setRouteLeaveHook still updating URL - javascript

I'm using react 15.3.1, with react-router 2.4.1 and react-router-redux 4.0.5:
When I trap the routing change with:
this.props.router.setRouteLeaveHook(
this.props.route,
this.routerWillLeave
);
private routerWillLeave = () => {
if (this.state.editing)
return 'You may have unsaved changes. Are you sure you want to leave?'
};
... I do get my this.routerWillLeave method called, but the URL in the browser still changes, so even if the user stays on the page by deciding not to leave the page, the URL is now wrong. Ideas?

export default class extends Component {
static contextTypes = {
router: React.PropTypes.object.isRequired
}
state = {
editing: true
}
componentDidMount() {
this.context.router.setRouteLeaveHook(this.props.route, () => {
if (this.state.editing) {
return false;
// At here you can give a confirm dialog, return true when confirm true
}else {
return true;
}
})
}
}

And if your react-route >2.4, you can also use withRouter to wrap your component, this's may be better!
import React, {Component} from 'react';
import {render} from 'react-dom';
import {withRouter} from 'react-router';
export default withRouter(class extends Component {
state = {
unsaved: true
}
componentDidMount() {
this.props.router.setRouteLeaveHook(this.props.route, () => {
if (this.state.unsaved) {
return false;
// At here you can give a confirm dialog, return true when confirm true
}else {
return true;
}
})
}
render() {
return (
<div>
<h2>About</h2>
{this.props.children || "This is outbox default!"}
</div>
)
}
})

Related

How to pass in an instance variable from a React component to its HOC?

I typically use component composition to reuse logic the React way. For example, here is a simplified version on how I would add interaction logic to a component. In this case I would make CanvasElement selectable:
CanvasElement.js
import React, { Component } from 'react'
import Selectable from './Selectable'
import './CanvasElement.css'
export default class CanvasElement extends Component {
constructor(props) {
super(props)
this.state = {
selected: false
}
this.interactionElRef = React.createRef()
}
onSelected = (selected) => {
this.setState({ selected})
}
render() {
return (
<Selectable
iElRef={this.interactionElRef}
onSelected={this.onSelected}>
<div ref={this.interactionElRef} className={'canvas-element ' + (this.state.selected ? 'selected' : '')}>
Select me
</div>
</Selectable>
)
}
}
Selectable.js
import { Component } from 'react'
import PropTypes from 'prop-types'
export default class Selectable extends Component {
static propTypes = {
iElRef: PropTypes.shape({
current: PropTypes.instanceOf(Element)
}).isRequired,
onSelected: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
selected: false
}
}
onClick = (e) => {
const selected = !this.state.selected
this.setState({ selected })
this.props.onSelected(selected)
}
componentDidMount() {
this.props.iElRef.current.addEventListener('click', this.onClick)
}
componentWillUnmount() {
this.props.iElRef.current.removeEventListener('click', this.onClick)
}
render() {
return this.props.children
}
}
Works well enough. The Selectable wrapper does not need to create a new div because its parent provides it with a reference to another element that is to become selectable.
However, I've been recommended on numerous occasions to stop using such Wrapper composition and instead achieve reusability through Higher Order Components. Willing to experiment with HoCs, I gave it a try but did not come further than this:
CanvasElement.js
import React, { Component } from 'react'
import Selectable from '../enhancers/Selectable'
import flow from 'lodash.flow'
import './CanvasElement.css'
class CanvasElement extends Component {
constructor(props) {
super(props)
this.interactionElRef = React.createRef()
}
render() {
return (
<div ref={this.interactionElRef}>
Select me
</div>
)
}
}
export default flow(
Selectable()
)(CanvasElement)
Selectable.js
import React, { Component } from 'react'
export default function makeSelectable() {
return function decorateComponent(WrappedComponent) {
return class Selectable extends Component {
componentDidMount() {
// attach to interaction element reference here
}
render() {
return (
<WrappedComponent {...this.props} />
)
}
}
}
}
The problem is that there appears to be no obvious way to connect the enhanced component's reference (an instance variable) to the higher order component (the enhancer).
How would I "pass in" the instance variable (the interactionElRef) from the CanvasElement to its HOC?
I came up with a different strategy. It acts roughly like the Redux connect function, providing props that the wrapped component isn't responsible for creating, but the child is responsible for using them as they see fit:
CanvasElement.js
import React, { Component } from "react";
import makeSelectable from "./Selectable";
class CanvasElement extends Component {
constructor(props) {
super(props);
}
render() {
const { onClick, selected } = this.props;
return <div onClick={onClick}>{`Selected: ${selected}`}</div>;
}
}
CanvasElement.propTypes = {
onClick: PropTypes.func,
selected: PropTypes.bool,
};
CanvasElement.defaultProps = {
onClick: () => {},
selected: false,
};
export default makeSelectable()(CanvasElement);
Selectable.js
import React, { Component } from "react";
export default makeSelectable = () => WrappedComponent => {
const selectableFactory = React.createFactory(WrappedComponent);
return class Selectable extends Component {
state = {
isSelected: false
};
handleClick = () => {
this.setState({
isSelected: !this.state.isSelected
});
};
render() {
return selectableFactory({
...this.props,
onClick: this.handleClick,
selected: this.state.isSelected
});
}
}
};
https://codesandbox.io/s/7zwwxw5y41
I know that doesn't answer your question. I think you're trying to let the child get away without any knowledge of the parent.
The ref route feels wrong, though. I like the idea of connecting the tools to the child. You can respond to the click in either one.
Let me know what you think.
Just as you did on DOM element for CanvasElement, Ref can be attached to class component as well, checkout the doc for Adding a Ref to a Class Component
export default function makeSelectable() {
return function decorateComponent(WrappedComponent) {
return class Selectable extends Component {
canvasElement = React.createRef()
componentDidMount() {
// attach to interaction element reference here
console.log(this.canvasElement.current.interactionElRef)
}
render() {
return (
<WrappedComponent ref={this.canvasElement} {...this.props} />
)
}
}
}
}
Also, do checkout Ref forwarding if you need child instance reference in ancestors that's multiple levels higher in the render tree. All those solutions are based on assumptions that you're on react 16.3+.
Some caveats:
In rare cases, you might want to have access to a child’s DOM node from a parent component. This is generally not recommended because it breaks component encapsulation, but it can occasionally be useful for triggering focus or measuring the size or position of a child DOM node.
While you could add a ref to the child component, this is not an ideal solution, as you would only get a component instance rather than a DOM node. Additionally, this wouldn’t work with functional components. https://reactjs.org/docs/forwarding-refs.html
I've now come up with an opinionated solution where the HoC injects two callback functions into the enhanced component, one to register the dom reference and another to register a callback that is called when an element is selected or deselected:
makeElementSelectable.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import movementIsStationary from '../lib/movement-is-stationary';
/*
This enhancer injects the following props into your component:
- setInteractableRef(node) - a function to register a React reference to the DOM element that should become selectable
- registerOnToggleSelected(cb(bool)) - a function to register a callback that should be called once the element is selected or deselected
*/
export default function makeElementSelectable() {
return function decorateComponent(WrappedComponent) {
return class Selectable extends Component {
static propTypes = {
selectable: PropTypes.bool.isRequired,
selected: PropTypes.bool
}
eventsAdded = false
state = {
selected: this.props.selected || false,
lastDownX: null,
lastDownY: null
}
setInteractableRef = (ref) => {
this.ref = ref
if (!this.eventsAdded && this.ref.current) {
this.addEventListeners(this.ref.current)
}
// other HoCs may set interactable references too
this.props.setInteractableRef && this.props.setInteractableRef(ref)
}
registerOnToggleSelected = (cb) => {
this.onToggleSelected = cb
}
componentDidMount() {
if (!this.eventsAdded && this.ref && this.ref.current) {
this.addEventListeners(this.ref.current)
}
}
componentWillUnmount() {
if (this.eventsAdded && this.ref && this.ref.current) {
this.removeEventListeners(this.ref.current)
}
}
/*
keep track of where the mouse was last pressed down
*/
onMouseDown = (e) => {
const lastDownX = e.clientX
const lastDownY = e.clientY
this.setState({
lastDownX, lastDownY
})
}
/*
toggle selected if there was a stationary click
only consider clicks on the exact element we are making interactable
*/
onClick = (e) => {
if (
this.props.selectable
&& e.target === this.ref.current
&& movementIsStationary(this.state.lastDownX, this.state.lastDownY, e.clientX, e.clientY)
) {
const selected = !this.state.selected
this.onToggleSelected && this.onToggleSelected(selected, e)
this.setState({ selected })
}
}
addEventListeners = (node) => {
node.addEventListener('click', this.onClick)
node.addEventListener('mousedown', this.onMouseDown)
this.eventsAdded = true
}
removeEventListeners = (node) => {
node.removeEventListener('click', this.onClick)
node.removeEventListener('mousedown', this.onMouseDown)
this.eventsAdded = false
}
render() {
return (
<WrappedComponent
{...this.props}
setInteractableRef={this.setInteractableRef}
registerOnToggleSelected={this.registerOnToggleSelected} />
)
}
}
}
}
CanvasElement.js
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import PropTypes from 'prop-types'
import flowRight from 'lodash.flowright'
import { moveSelectedElements } from '../actions/canvas'
import makeElementSelectable from '../enhancers/makeElementSelectable'
class CanvasElement extends PureComponent {
static propTypes = {
setInteractableRef: PropTypes.func.isRequired,
registerOnToggleSelected: PropTypes.func
}
interactionRef = React.createRef()
componentDidMount() {
this.props.setInteractableRef(this.interactionRef)
this.props.registerOnToggleSelected(this.onToggleSelected)
}
onToggleSelected = async (selected) => {
await this.props.selectElement(this.props.id, selected)
}
render() {
return (
<div ref={this.interactionRef}>
Select me
</div>
)
}
}
const mapStateToProps = (state, ownProps) => {
const {
canvas: {
selectedElements
}
} = state
const selected = !!selectedElements[ownProps.id]
return {
selected
}
}
const mapDispatchToProps = dispatch => ({
selectElement: bindActionCreators(selectElement, dispatch)
})
const ComposedCanvasElement = flowRight(
connect(mapStateToProps, mapDispatchToProps),
makeElementSelectable()
)(CanvasElement)
export default ComposedCanvasElement
This works, but I can think of at least one significant issue: the HoC injects 2 props into the enhanced component; but the enhanced component has no way of declaratively defining which props are injected and just needs to "trust" that these props are magically available
Would appreciate feedback / thoughts on this approach. Perhaps there is a better way, e.g. by passing in a "mapProps" object to makeElementSelectable to explicitly define which props are being injected?

Call a React component to show up like an alert function

I have a react component which shows a message:
Here's the code:
import React, { PropTypes } from 'react';
const Message = props => {
const { type, msg } = props;
if (type === 'success') {
return (<div>{msg}</div>);
} else {
return null;
}
};
Message.PropTypes = {
type: PropTypes.string.isRequired,
msg: PropTypes.string.isRequired,
};
export default Message;
//This component is called like this from index.js:
<Message type="success" msg="This is a Message" />
My question is...How can I call the component like I would call a function.
For example:
if (function is success then) {
//Show the Message Component
}
How can I do this with React?
If the if clause is within another React component you'd just render it,
class AnotherReact extends Component {
render() {
let alert = success ? <Message /> else '';
return (<div>{ alert }</div>);
}
}
Otherwise if not in a React component then you would have to use ReactDOM.render().
if (is success) {
ReactDOM.render(<Message />, document.querySelector());
}

changing the value of one object to true or false in redux

I'm trying change state of checkbox in redux. Everything is set up correctly except the last part. I try to send a "false" or "true" and the decide in action what to call but I get this error
Uncaught TypeError: Cannot read property 'payload' of undefined
This is my code
export function isChecked(isCheck) {
if(isCheck == false){
return{
type: "IS_TRUE",
isCheck
}
} else if (isCheck == true){
return{
type: "IS_FALSE",
isCheck
}
}
}
If I put just else then it works fine but then second time around it doesn't change
You are using redux in a wrong way, here is an example for checkbox:
// action
export const SET_CHECKBOX_VALUE = 'SET_CHECKBOX_VALUE ';
export function setChecked(checked) {
return {
type: SET_CHECKBOX_VALUE,
checked
}
}
// filter/Reducer
import { SET_CHECKBOX_VALUE } from '..your action file here';
const initialCheckboxState = { isChecked: false };
export default function checkBoxFilter(state, action) {
switch (action.type) {
case SET_CHECKBOX_VALUE :
return { checked: action.checked}
default:
return initialCheckboxState;
}
}
// component
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as checkBoxActions from '..your action file here';
class MyComponent extends Component {
static propTypes = {
isChecked: React.PropTypes.bool.isRequired,
setChecked: React.PropTypes.func.isRequired
};
render() {
return (
<div onClick={() => { this.props.setChecked(!this.props.isChecked) }>
{this.props.isChecked}
</div>
);
}
}
function mapStateToProps(state) {
return {
isChecked: state.checkBoxFilter.checked
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(checkBoxActions, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);

Dispatch() calls a function but .then() doesn't work on React-Redux

I'm creating my first React-Redux App. I'm using yo generator-redux and following this repo and official documenation. I have rendered de SignIn Presentational Component and it works fine, show errors if inputs are blanks. The problem is at dispatching. I use Thunk Middleware but the repo doesn't.
I have used console.log() to explore how deeper is working my code and I found that the Component Actions are being called, the AJAX request (with axios) is working fine, but the .then() (I think) is not working but doesn't throw errors.
This is my code:
Action
actions/UsersActions.js
import axios from 'axios';
//sign in user
export const SIGNIN_USER = 'SIGNIN_USER';
export const SIGNIN_USER_SUCCESS = 'SIGNIN_USER_SUCCESS';
export const SIGNIN_USER_FAILURE = 'SIGNIN_USER_FAILURE';
//Get current user(me) from token in localStorage
export const ME_FROM_TOKEN = 'ME_FROM_TOKEN';
export const ME_FROM_TOKEN_SUCCESS = 'ME_FROM_TOKEN_SUCCESS';
export const ME_FROM_TOKEN_FAILURE = 'ME_FROM_TOKEN_FAILURE';
export const RESET_TOKEN = 'RESET_TOKEN';
//log out user
export const LOGOUT_USER = 'LOGOUT_USER';
axios.defaults.baseURL = location.href.indexOf('10.1.1.33') > 0 ? 'http://10.1.1.33:8080/api/v1' : 'http://10.1.1.33:8080/api/v1';
export function signInUser(formValues) {
const request = axios.post('/login', formValues);
console.log(request);
// It works fine and receives the resposen when is invoked from Container
return {
type: SIGNIN_USER,
payload: request
};
}
export function signInUserSuccess(user) {
return {
type: SIGNIN_USER_SUCCESS,
payload: user
}
}
export function signInUserFailure(error) {
return {
type: SIGNIN_USER_FAILURE,
payload: error
}
}
export function meFromToken(tokenFromStorage) {
//check if the token is still valid, if so, get me from the server
const request = axios.get('/me/from/token?token=${tokenFromStorage}');
return {
type: ME_FROM_TOKEN,
payload: request
};
}
export function meFromTokenSuccess(currentUser) {
return {
type: ME_FROM_TOKEN_SUCCESS,
payload: currentUser
};
}
export function meFromTokenFailure(error) {
return {
type: ME_FROM_TOKEN_FAILURE,
payload: error
};
}
export function resetToken() {//used for logout
return {
type: RESET_TOKEN
};
}
export function logOutUser() {
return {
type: LOGOUT_USER
};
}
Component
components/SignInForm.js
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
class SignInForm extends Component {
static contextTypes = {
router: PropTypes.object
};
componentWillUnmount() {
// Invoked immediately before a component is unmounted from the DOM.
// Perform any necessary cleanup in this method, such as invalidating timers or
// cleaning up any DOM elements that were created in componentDidMount.
// Important! If your component is navigating based on some global state(from say componentWillReceiveProps)
// always reset that global state back to null when you REMOUNT
this.props.resetMe();
}
componentWillReceiveProps(nextProps) {
// Invoked when a component is receiving new props. This method is not called for the initial render.
if(nextProps.user && nextProps.user.status === 'authenticated' && nextProps.user.user && !nextProps.user.error) {
this.context.router.push('/');
}
//error
//Throw error if it was not already thrown (check this.props.user.error to see if alert was already shown)
//If u dont check this.props.user.error, u may throw error multiple times due to redux-form's validation errors
if(nextProps.user && nextProps.user.status === 'signin' && !nextProps.user.user && nextProps.user.error && !this.props.user.error) {
alert(nextProps.user.error.message);
}
}
render() {
const { asyncValidating, fields: { email, password }, handleSubmit, submitting, user } = this.props;
return (
<div>
<form onSubmit={handleSubmit(this.props.signInUser.bind(this))}>
<div>
<label>Email</label>
<input type="text" placeholder="email#4geeks.com.ve" {...email} />
<div>{email.touched ? email.error : ''}</div>
<div>{ asyncValidating === 'email' ? 'validating...' : ''}</div>
</div>
<div>
<label>Password</label>
<input type="password" {...password} />
<div>{password.touched ? password.error : ''}</div>
<div>{ asyncValidating === 'password' ? 'validating...' : ''}</div>
</div>
<button type="submit" disabled={submitting}>Submit</button>
</form>
</div>
);
}
}
export default SignInForm;
Container
containers/SignInFormContainer.js
import { reduxForm } from 'redux-form';
import SignInForm from '../components/SignInForm';
import { signInUser, signInUserSuccess, signInUserFailure } from '../actions/UsersActions';
// Client side validation
function validate(values) {
var errors = {};
var hasErrors = false;
if(!values.email || values.email.trim() == '') {
errors.email = "Enter a registered email.";
hasErrors = true;
}
if(!values.password || values.password.trim() == '') {
errors.password = "Enter password.";
hasErrors = true;
}
return hasErrors && errors;
}
// For any field errors upon submission (i.e. not instant check)
const validateAndSignInUser = (values, dispatch) => {
return new Promise ((resolve, reject) => {
console.log('this is showed');
dispatch(signInUser(values))
.then((response) => {
console.log('this console.log is not showed');
let data = response.payload.data;
// if any one of these exist, then there is a field error
if(response.payload.status != 200) {
// let other components know of error by updating the redux` state
dispatch(signInUserFailure(response.payload));
reject(data); // this is for redux-form itself
} else {
// store JWT Token to browser session storage
// If you use localStorage instead of sessionStorage, then this w/ persisted across tabs and new windows.
// sessionStorage = persisted only in current tab
sessionStorage.setItem('dhfUserToken', response.payload.data.token);
// let other components know that we got user and things are fine by updating the redux` state
dispatch(signInUserSuccess(response.payload));
resolve(); // this is for redux-form itself
}
});
});
}
const mapDispatchToProps = (dispatch) => {
return {
signInUser: validateAndSignInUser
}
}
function mapStateToProps(state, ownProps) {
return {
user: state.user
};
}
// connect: first argument is mapStateToProps, 2nd is mapDispatchToProps
// reduxForm: 1st is form config, 2nd is mapStateToProps, 3rd is mapDispatchToProps
export default reduxForm({
form: 'SignInForm',
fields: ['email', 'password'],
null,
null,
validate
}, mapStateToProps, mapDispatchToProps)(SignInForm);
Presentational/Page/View
presentational/SignIn.js
import React, { Component } from 'react';
import HeaderContainer from '../containers/HeaderContainer';
import SignInFormContainer from '../containers/SignInFormContainer';
class SignIn extends Component {
render() {
return (
<div>
<HeaderContainer />
<SignInFormContainer />
</div>
);
}
}
export default SignIn;
Reducers
reducres/UserReducer.js
import {
ME_FROM_TOKEN, ME_FROM_TOKEN_SUCCESS, ME_FROM_TOKEN_FAILURE, RESET_TOKEN,
SIGNIN_USER, SIGNIN_USER_SUCCESS, SIGNIN_USER_FAILURE,
LOGOUT_USER
} from '../actions/UsersActions';
const INITIAL_STATE = {user: null, status:null, error:null, loading: false};
export default function(state = INITIAL_STATE, action) {
let error;
switch(action.type) {
case ME_FROM_TOKEN:// loading currentUser("me") from jwttoken in local/session storage storage,
return { ...state, user: null, status:'storage', error:null, loading: true};
case ME_FROM_TOKEN_SUCCESS://return user, status = authenticated and make loading = false
return { ...state, user: action.payload.data.user, status:'authenticated', error:null, loading: false}; //<-- authenticated
case ME_FROM_TOKEN_FAILURE:// return error and make loading = false
error = action.payload.data || {message: action.payload.message};//2nd one is network or server down errors
return { ...state, user: null, status:'storage', error:error, loading: false};
case RESET_TOKEN:// remove token from storage make loading = false
return { ...state, user: null, status:'storage', error:null, loading: false};
case SIGNIN_USER:// sign in user, set loading = true and status = signin
return { ...state, user: null, status:'signin', error:null, loading: true};
case SIGNIN_USER_SUCCESS://return authenticated user, make loading = false and status = authenticated
return { ...state, user: action.payload.data.user, status:'authenticated', error:null, loading: false}; //<-- authenticated
case SIGNIN_USER_FAILURE:// return error and make loading = false
error = action.payload.data || {message: action.payload.message};//2nd one is network or server down errors
return { ...state, user: null, status:'signin', error:error, loading: false};
case LOGOUT_USER:
return {...state, user:null, status:'logout', error:null, loading: false};
default:
return state;
}
}
reducers/index.js
import { combineReducers } from 'redux';
import { UserReducer } from './UserReducer';
import { reducer as formReducer } from 'redux-form';
const rootReducer = combineReducers({
user: UserReducer,
form: formReducer // <-- redux-form
});
export default rootReducer;
Store
import {createStore, applyMiddleware, combineReducers, compose} from 'redux';
import thunkMiddleware from 'redux-thunk';
import {devTools, persistState} from 'redux-devtools';
import rootReducer from '../reducers/index';
let createStoreWithMiddleware;
// Configure the dev tools when in DEV mode
if (__DEV__) {
createStoreWithMiddleware = compose(
applyMiddleware(thunkMiddleware),
devTools(),
persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))
)(createStore);
} else {
createStoreWithMiddleware = applyMiddleware(thunkMiddleware)(createStore);
}
export default function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState);
}
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import { Router, browserHistory } from 'react-router';
import routes from './routes';
import configureStore from './store/configureStore';
import {renderDevTools} from './utils/devTools';
const store = configureStore();
ReactDOM.render(
<div>
{/* <Home /> is your app entry point */}
<Provider store={store}>
<Router history={browserHistory} routes={routes} />
</Provider>
{/* only renders when running in DEV mode */
renderDevTools(store)
}
</div>
, document.getElementById('main'));
I hope you can help me! I don't know if something is wrong because I'm using Thunk and the example don't, or if something is missging.
Thank you guys!
It looks like you are using redux-thunk where as I am using redux-promise middlewares. They are totally different. You should change redux-thunk to redux-promise if you want to use the repo
I solved my issue. The difference is that I need to process de Promise received from signInUser throug the attribute that has it.
I had to receive the response in response and then access to the Promise in response.payload. In addition, I had to use .then() and .catch() to handle it.
// For any field errors upon submission (i.e. not instant check)
const validateAndSignInUser = (values, dispatch) => {
return new Promise ((resolve, reject) => {
let response = dispatch(signInUser(values));
response.payload.then((payload) => {
// if any one of these exist, then there is a field error
if(payload.status != 200) {
// let other components know of error by updating the redux` state
dispatch(signInUserFailure(payload));
reject(payload.data); // this is for redux-form itself
} else {
// store JWT Token to browser session storage
// If you use localStorage instead of sessionStorage, then this w/ persisted across tabs and new windows.
// sessionStorage = persisted only in current tab
sessionStorage.setItem('dhfUserToken', payload.data.token);
// let other components know that we got user and things are fine by updating the redux` state
dispatch(signInUserSuccess(payload));
resolve(); // this is for redux-form itself
}
}).catch((payload) => {
// let other components know of error by updating the redux` state
sessionStorage.removeItem('dhfUserToken');
dispatch(signInUserFailure(payload));
reject(payload.data); // this is for redux-form itself
});
});
}

How to use a custom component with react-router route transitions?

The article Confirming Navigation explains how to use a browser confirmation box in your transition hook. Fine. But I want to use my own Dialog box. If I were to use the methods from the history module I think this is possible. Is it possible to do this with the setRouteLeaveHook in react-router?
The core problem is that setRouteLeaveHook expects the hook function to return its result synchronously. This means you don't have the time to display a custom dialog component, wait for the user to click an option, and then return the result. So we need a way to specify an asynchronous hook. Here's a utility function I wrote:
// Asynchronous version of `setRouteLeaveHook`.
// Instead of synchronously returning a result, the hook is expected to
// return a promise.
function setAsyncRouteLeaveHook(router, route, hook) {
let withinHook = false
let finalResult = undefined
let finalResultSet = false
router.setRouteLeaveHook(route, nextLocation => {
withinHook = true
if (!finalResultSet) {
hook(nextLocation).then(result => {
finalResult = result
finalResultSet = true
if (!withinHook && nextLocation) {
// Re-schedule the navigation
router.push(nextLocation)
}
})
}
let result = finalResultSet ? finalResult : false
withinHook = false
finalResult = undefined
finalResultSet = false
return result
})
}
Here is an example of how to use it, using vex to show a dialog box:
componentWillMount() {
setAsyncRouteLeaveHook(this.context.router, this.props.route, this.routerWillLeave)
}
​
routerWillLeave(nextLocation) {
return new Promise((resolve, reject) => {
if (!this.state.textValue) {
// No unsaved changes -- leave
resolve(true)
} else {
// Unsaved changes -- ask for confirmation
vex.dialog.confirm({
message: 'There are unsaved changes. Leave anyway?' + nextLocation,
callback: result => resolve(result)
})
}
})
}
I made it work by setting a boolean on state whether you have confirmed to navigate away (using react-router 2.8.x). As it says in the link you posted:
https://github.com/ReactTraining/react-router/blob/master/docs/guides/ConfirmingNavigation.md
return false to prevent a transition w/o prompting the user
However, they forget to mention that the hook should be unregistered as well, see here and here.
We can use this to implement our own solution as follows:
class YourComponent extends Component {
constructor() {
super();
const {route} = this.props;
const {router} = this.context;
this.onCancel = this.onCancel.bind(this);
this.onConfirm = this.onConfirm.bind(this);
this.unregisterLeaveHook = router.setRouteLeaveHook(
route,
this.routerWillLeave.bind(this)
);
}
componentWillUnmount() {
this.unregisterLeaveHook();
}
routerWillLeave() {
const {hasConfirmed} = this.state;
if (!hasConfirmed) {
this.setState({showConfirmModal: true});
// Cancel route change
return false;
}
// User has confirmed. Navigate away
return true;
}
onCancel() {
this.setState({showConfirmModal: false});
}
onConfirm() {
this.setState({hasConfirmed: true, showConfirmModal: true}, function () {
this.context.router.goBack();
}.bind(this));
}
render() {
const {showConfirmModal} = this.state;
return (
<ConfirmModal
isOpen={showConfirmModal}
onCancel={this.onCancel}
onConfirm={this.onConfirm} />
);
}
}
YourComponent.contextTypes = {
router: routerShape
};
Posting my solution for intercept back button or even a route change. This works with React-router 2.8 or higher. Or even with withRouter
import React, {PropTypes as T} from 'react';
...
componentWillMount() {
this.context.router.setRouteLeaveHook(this.props.route, this.routerWillLeaveCallback.bind(this));
}
routerWillLeaveCallback(nextLocation) {
let showModal = this.state.unsavedChanges;
if (showModal) {
this.setState({
openUnsavedDialog: true,
unsavedResolveCallback: Promise.resolve
});
return false;
}
return true;
}
}
YourComponent.contextTypes = {
router: T.object.isRequired
};
Above is great except when user goes back in history. Something like the following should fix the problem:
if (!withinHook && nextLocation) {
if (nextLocation.action=='POP') {
router.goBack()
} else {
router.push(nextLocation)
}
}
Here's my solution to the same. I made a custom dialog component that you can use to wrap any component in your app. You can wrap your header and this way have it present on all pages. It assumes you're using Redux Form, but you can simply replace areThereUnsavedChanges with some other form change checking code. It also uses React Bootstrap modal, which again you can replace with your own custom dialog.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { withRouter, browserHistory } from 'react-router'
import { translate } from 'react-i18next'
import { Button, Modal, Row, Col } from 'react-bootstrap'
// have to use this global var, because setState does things at unpredictable times and dialog gets presented twice
let navConfirmed = false
#withRouter
#connect(
state => ({ form: state.form })
)
export default class UnsavedFormModal extends Component {
constructor(props) {
super(props)
this.areThereUnsavedChanges = this.areThereUnsavedChanges.bind(this)
this.state = ({ unsavedFormDialog: false })
}
areThereUnsavedChanges() {
return this.props.form && Object.values(this.props.form).length > 0 &&
Object.values(this.props.form)
.findIndex(frm => (Object.values(frm)
.findIndex(field => field && field.initial && field.initial !== field.value) !== -1)) !== -1
}
render() {
const moveForward = () => {
this.setState({ unsavedFormDialog: false })
navConfirmed = true
browserHistory.push(this.state.nextLocation.pathname)
}
const onHide = () => this.setState({ unsavedFormDialog: false })
if (this.areThereUnsavedChanges() && this.props.router && this.props.routes && this.props.routes.length > 0) {
this.props.router.setRouteLeaveHook(this.props.routes[this.props.routes.length - 1], (nextLocation) => {
if (navConfirmed || !this.areThereUnsavedChanges()) {
navConfirmed = false
return true
} else {
this.setState({ unsavedFormDialog: true, nextLocation: nextLocation })
return false
}
})
}
return (
<div>
{this.props.children}
<Modal show={this.state.unsavedFormDialog} onHide={onHide} bsSize="sm" aria-labelledby="contained-modal-title-md">
<Modal.Header>
<Modal.Title id="contained-modal-title-md">WARNING: unsaved changes</Modal.Title>
</Modal.Header>
<Modal.Body>
Are you sure you want to leave the page without saving changes to the form?
<Row>
<Col xs={6}><Button block onClick={onHide}>Cancel</Button></Col>
<Col xs={6}><Button block onClick={moveForward}>OK</Button></Col>
</Row>
</Modal.Body>
</Modal>
</div>
)
}
}

Categories