I have the react-router-redux set up as this example. But dispatch(push(url)) does not change the content/view nor the url on address bar. Even though from my console I can see that the LOCATION_CHANGE and CALL_HISTORY_METHOD are successfully called with my given address. In this case, if the sign in is success, it does not load the expected redirect address.
a sign in action
export function loginUser(email, password, redirect="/dashboard") {
return function(dispatch) {
dispatch(loginUserRequest());
return fetch(`http://localhost:3000/users/sign_in/`, {
...
})
.then(parseJSON)
.then(response => {
try {
let decoded = jwtDecode(response.token);
dispatch(loginUserSuccess(response.token));
dispatch(push(redirect));
} catch (e) {
...
}
})
}
}
routes.js
import React from 'react';
import { Router, Route, browserHistory, IndexRoute } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux'
...
import store from '../store';
const history = syncHistoryWithStore(browserHistory, store)
let Routes =
<Router history={history}>
<Route path='/' component={MainContainer}>
<IndexRoute component={Home} />
<Route path='/sign_in' component={SigninForm} />
<Route path='dashboard' component={authenticateComponent(Dashboard)} />
</Route>
</Router>
export default Routes;
store.js
import { createStore, applyMiddleware, compose } from 'redux'
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import reducers from './reducers';
const createStoreWithMiddleware = compose(
applyMiddleware(
thunkMiddleware,
createLogger()
)
)
const store = createStore(reducers, createStoreWithMiddleware);
export default store;
AuthenticateComponent.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { push } from 'react-router-redux';
export function authenticateComponent(Component) {
class AuthenticateComponent extends Component {
componentWillMount () {
this.checkAuth(this.props.isAuthenticated);
}
componentWillReceiveProps (nextProps) {
this.checkAuth(nextProps.isAuthenticated);
}
checkAuth (isAuthenticated) {
if (!isAuthenticated) {
let redirectAfterLogin = this.props.location.pathname;
this.props.dispatch(push(`/sign_in?next=${redirectAfterLogin}`));
}
}
render () {
return (
<div>
{this.props.isAuthenticated === true
? <Component {...this.props}/>
: null
}
</div>
)
}
}
const mapStateToProps = (state) => ({
token: state.auth.token,
isAuthenticated: state.auth.isAuthenticated
});
return connect(mapStateToProps)(AuthenticateComponent);
}
I have put the routing: routerReducer into my combined reducers as well. What could be the problem of this?
Turns out I need to apply the routerMiddleware as well
Related
I am developing a React JS, Redux, GraphQL, TypeScript app.
And I would like to know how to invoke the function that fetches data and updates the state via GraphQL from my container.
The name of the action that loads the data via GraphQL is appActions.getAppData();
But it causes an infinite refresh loop because it triggers (StatusActions.startAppLoading()); which updates the state as well.
I would like to know how to fix this issue or how to rewrite /Main/index.tsx as a class component and invoke startAppLoading() from componentDidMount().
Thank you in advance.
main.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createBrowserHistory } from 'history';
import { configureStore } from 'app/store';
import { Router } from 'react-router';
import { App } from './app';
// prepare store
const history = createBrowserHistory();
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<App />
</Router>
</Provider>,
document.getElementById('root')
);
app/index.tsx
import React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { App as Main } from 'app/containers/Main';
import { hot } from 'react-hot-loader';
let currentContainer = Main;
export const App = hot(module)(() => (
<Switch>
<Route exact path="/" component={currentContainer} />
<Route path="*">
<Redirect to="https://google.com" />
</Route>
</Switch>
));
app/containers/Main/index.tsx
import React from 'react';
import style from './style.css';
import { RouteComponentProps } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import { useTodoActions } from 'app/actions';
import { useAppActions } from 'app/actions';
import { RootState } from 'app/reducers';
import { Header, TodoList, Footer } from 'app/components';
export namespace App {
export interface Props extends RouteComponentProps<void> {}
}
export const App = ({ history, location }: App.Props) => {
const dispatch = useDispatch();
const appActions = useAppActions(dispatch);
const { apps } = useSelector((state: RootState) => {
return {
apps: state.apps
};
});
appActions.getAppData();
return (
<div className={style.normal}>
<Header />
<TodoList appActions={appActions} apps={apps} />
<Footer />
</div>
);
};
app/actions/apps.ts
export const getAppData = () => {
let appKey = 'interpegasus';
return (dispatch: Dispatch) => {
dispatch(StatusActions.startAppLoading());
debugger;
apolloClient
.query({
query: gql`
query getApp($appKey: String!) {
getApp(id: $appKey) {
id
name
domain
}
}
`,
variables: {
appKey: appKey
}
})
.then((result) => {
debugger;
if (result.data.apps.length > 0) {
dispatch(populateAppData(result.data.apps[0]));
}
dispatch(StatusActions.endAppLoading());
})
.catch((error) => {
dispatch(StatusActions.endAppLoading());
console.log({
error: error
});
});
};
};
You should put your appActions.getAppData() inside useEffect hooks like this
useEffect(()=>{
appActions.getAppData()
},[])
check the official docs Introducing Hooks
In Main/index.tsx, you are calling appActions.getAppData(); which will lead you to actions/apps.ts. Here, you are doing dispatch(StatusActions.startAppLoading()); which will update the state and re-render ``Main/index.tsx`. Then again you call getAppData() and the loop continues to lead to infinite loop.
Call the api only if not loading.
Something like this:
...
const { apps, loading } = useSelector((state: RootState) => {
return {
apps: state.apps,
loading: state.loading // <----- replace with your actual name of your state
};
});
if(!loading){
appActions.getAppData();
}
...
I would like my saga to redirect a logging in user to a main dashboard screen after successful authentication. I followed the code outlined in the following article: https://github.com/supasate/connected-react-router and example from https://github.com/supasate/connected-react-router/tree/master/examples/basic. I can see the url change when the push command is called from within the saga, however, the page is not redirecting to the dashboard screen. Instead, the user just stays on the same screen. I am currently running react-redux v6, react v16.4.1, react-router: v4.3.1, react-router-redux v4.0.8. What could I be doing wrong?
app.js:
class App extends Component {
constructor() {
super();
this.state = {
loading: true,
loaded: false
}
}
componentDidMount() {
window.addEventListener('load', () => {
this.setState({loading: false});
setTimeout(() => this.setState({loaded: true}), 500);
});
}
render() {
const loaded = this.state.loaded;
const {history} = this.props;
return (
<div>
{!loaded && <div className={`load${this.state.loading ? '' : ' loaded'}`}>
<div className='load__icon-wrap'>
<svg className='load__icon'>
<path fill='#4ce1b6' d='M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z'/>
</svg>
</div>
</div>}
<div>
<ConnectedRouter history={history}>
<Router/>
</ConnectedRouter>
</div>
</div>
)
}
}
history.js:
import { createBrowserHistory } from 'history';
export default createBrowserHistory;
store.js:
import {createStore, applyMiddleware} from 'redux';
import {reducer as reduxFormReducer} from 'redux-form';
import createSagaMiddleware from 'redux-saga';
import sagas from '../redux/sagas';
import reducers from '../redux/reducers';
import { createBrowserHistory, } from 'history';
import { routerMiddleware } from 'connected-react-router';
const history = createBrowserHistory({ basename: '/myrootpath' });
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducers(history), applyMiddleware(routerMiddleware(history), sagaMiddleware));
store.runSaga = sagaMiddleware.run(sagas);
// store.asyncReducers = {};
const action = type => store.dispatch({type })
export default store;
index.js
import React from 'react';
import App from './app/App';
import {render} from 'react-dom'
import {Provider} from 'react-redux'
import {BrowserRouter} from 'react-router-dom';
import store from './app/store';
import ScrollToTop from './app/ScrollToTop';
import { createBrowserHistory } from 'history';
const history = createBrowserHistory()
render(
<Provider store={store}>
<BrowserRouter basename='/myrootpath'>
<ScrollToTop>
<App history={history}/>
</ScrollToTop>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
saga code:
function* login(params){
try {
const api = new UserApi();
const {username, password} = params.login;
const user = yield call(api.login, {username, password});
debugger;
yield put(push('/dashboard_default'));
} catch (error) {
debugger;
yield put({ type: types.LOAD_ERROR, error });
}
}
The same history object needs to be used in both places, e.g. in history.js:
export default createBrowserHistory({basename: '/myrootpath'});
and in index.js and store.js:
import history from './history';
On onClick SigninButton calls ONTOGGLE_MODAL_SIGNIN which updates ui.isSigninModalActive from store. Everything works fine but I see that my whole app gets re-rendered when ui.isSigninModalActive toggles on and off. Is this normal behaviour? I had thought that you have to store.subscribe and update that component's inner state and that component alone gets updated (and not the whole app) when store updates. If the whole app re-renders, then what is the point of store.subscribe? Or did I mess up somewhere? Thanks for the help in advance.
signin_button.jsx
import React, { Component } from 'react';
import { store } from '../../../store/store';
import { onToggleModal } from '../../../actions/ui';
export const SigninButton = () => (
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" className="signin-button"
onClick={ () => store.dispatch(onToggleModal('signin')) }>
<path d="M0 0h24.997C25.55 0 26 .444 26 1c0 .552-.45 1-1.003 1H0V0"/>
</svg>
);
router.js
import React from 'react';
import { render } from 'react-dom';
import { Router, IndexRoute } from 'react-router';
import { Provider } from 'react-redux';
import { store, history } from '../../store/store';
import App from '../../ui/containers/app_container';
import { Welcome } from '../../ui/pages/welcome';
Meteor.startup(() => {
render(
<Provider store={ store }>
<Router history={ history }>
<Route path="/" component={ App }>
<IndexRoute component={ Welcome } />
</Route>
</Router>
</Provider>,
document.getElementById('root'));
});
store.js
import { createStore } from 'redux';
import { syncHistoryWithStore } from 'react-router-redux';
import { browserHistory } from 'react-router';
import { rootReducer } from '../reducers/root_reducer';
import { ui } from './ui_store';
const defaultState = { ui };
export const store = createStore(rootReducer, defaultState,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
export const history = syncHistoryWithStore(browserHistory, store);
ui_store.js
export const ui = {
isSigninModalActive: false,
};
root_reducer.js
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import { ui } from './ui_reducer';
export const rootReducer = combineReducers({ ui, routing: routerReducer });
ui_reducer.js
import update from 'immutability-helper';
import { toggleBodyOverflow } from '../modules/toggle_body_overflow';
export const ui = (state = null, action) => {
switch (action.type) {
case 'ONTOGGLE_MODAL_SIGNIN': {
toggleBodyOverflow(!state.isSigninModalActive);
document.getElementById('signin-modal__container').classList.toggle('active');
return update(state, { isSigninModalActive: { $set: !state.isSigninModalActive } });
}
default: return state;
}
};
ui_action.js
export const onToggleModal = modal => ({ type: `ONTOGGLE_MODAL_${modal.toUpperCase()}` });
EDIT: I found the reason why app is re-rendering
On my app container, I have set mapStateToProps and sent the state.ui down the components as props. I "fixed" it by removing it. Is this the correct way to stop re-rendering the whole app?
app_container.js
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as ui from '../../actions/ui';
import { App } from '../layouts/app_layout';
// problem: const mapStateToProps = state => { ui: state.ui };
const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch => bindActionCreators(ui, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(App);
I am trying to implement a simple form logic for educational purposes. I am stuck trying to redirect to url on form submission. Here are relevant sections of my code;
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import { createStore, combineReducers, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import { Router, Route, IndexRoute, hashHistory } from 'react-router'
import { syncHistoryWithStore, routerReducer, routerMiddleware } from 'react-router-redux'
import { reducer as formReducer } from 'redux-form'
import {Home, Foo, Bar} from './components'
import {YirtibatLoginForm as LoginForm} from './containers/LoginForm'
import * as reducers from './reducers'
const reducer = combineReducers({
...reducers,
routing: routerReducer,
form: formReducer
})
const middleware = routerMiddleware(hashHistory)
const store = createStore(reducer, applyMiddleware(middleware))
const history = syncHistoryWithStore(hashHistory, store)
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={Home} />
<Route path="foo" component={Foo} />
<Route path="bar" component={Bar} />
<Route path="login" component={LoginForm} />
</Route>
</Router>
</Provider>,
document.getElementById('root')
);
containers/LoginForm.js
import React, { Component } from 'react';
import { connect } from 'react-redux'
import { push } from 'react-router'
import LoginForm from '../components/LoginForm'
export class BaseYirtibatLoginForm extends Component {
constructor() {
super();
this.handlesubmit = this.handlesubmit.bind(this);
}
handlesubmit(ev) {
this.props.submitting();
fetch('/login', {
method:'POST',
body:JSON.stringify(ev)
}).then(resp => {
if(!resp.ok) {
throw new Error(resp.statusText)
}
return resp.json()
}).then( resjson => {
this.props.submitsuccess(resjson)
}).catch(err => {
this.props.submiterror(err);
})
}
render() {
return (
<LoginForm onSubmit={this.handlesubmit} />
);
}
}
const mapStateToProps = (state) => {return {}}
const mapDispatchToProps = (dispatch) => {
return {
submitting: () => dispatch({type:'submitting'}),
submitsuccess: (data) => push("/success"),
submiterror: (err) => push("/error")
}
}
export const YirtibatLoginForm = connect(mapStateToProps, mapDispatchToProps)(BaseYirtibatLoginForm);
I think this code supposed to redirect hash url after the form has been submitted. However I am getting following error in browser console;
Uncaught (in promise) TypeError: (0 , _reactRouter.push) is not a function
at Object.submiterror (LoginForm.js:45)
at LoginForm.js:29
submiterror # LoginForm.js:45
(anonymous) # LoginForm.js:29
What is the prefered method to redirect to a route component after for submission events?
There is no push function exported by react-router. You could work with the history object directly, as mentioned in the comments, but the best way is to use the withRouter higher-order component. The code below touches the key points with inline comments.
// import
import { withRouter } from 'react-router'
...
export class BaseYirtibatLoginForm extends Component {
...
handlesubmit(ev) {
this.props.submitting();
fetch('/login', ...
).then( resjson => {
// take `router` from `this.props` and push new location
this.props.router.push("/success")
}).catch(err => {
// take `router` from `this.props` and push new location
this.props.router.push("/error")
})
}
}
const mapStateToProps = (state) => {return {}}
const mapDispatchToProps = (dispatch) => {
return {
submitting: () => dispatch({type:'submitting'}),
// redirect is not done through redux actions
}
}
// apply withRouter HoC
export const YirtibatLoginForm = withRouter(connect(mapStateToProps, mapDispatchToProps)(BaseYirtibatLoginForm));
I am using react-router and react-router-redux to handle navigation on my page. I need change my url programmatically inside component. I was trying to use this method: history.push to achieve this but this method is only change the url and component associated with this url is not updated. This app is simple list with pagination so when i switch to the next page url is changing for example /posts/1 to /posts/2 but view is not updated. I think this should work like this:
User click pagination item and click handler is called passing
page number as argument
Inside click handler i call history.push(/posts/[page]). I could
use Link component but i want to be able to do something when user
click pagination item
I expect that my ObjectList component will be mounted again and
componentDidMount will be called
This is probably not the best aproach so i will be greatfull for tips
links are hardcoded especially first argument
My source code:
client.js
import React from "react";
import ReactDOM from "react-dom";
import {Router, Route, IndexRoute, browserHistory} from "react-router";
import Results from "./views/Results";
import Home from "./views/Home";
import App from './components/App'
import { Provider } from 'react-redux';
import store, { history } from './store';
const app = document.getElementById('app');
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={Home} />
<Route path="/:category/:cityId/:pageNum" component={Results}></Route>
</Route>
</Router>
</Provider>,
app
);
store.js
import { createStore, compose, applyMiddleware } from 'redux'
import { syncHistoryWithStore } from 'react-router-redux'
import thunkMiddleware from 'redux-thunk'
import { browserHistory } from 'react-router'
import rootReducer from './reducers/index'
import createLogger from 'redux-logger'
import categories from './data/categories'
const loggerMiddleware = createLogger()
const defaultState = {
categories,
resultsList: {
objects: [],
counters: [],
isFetching: false
}
};
const store = createStore(
rootReducer,
defaultState,
compose (
applyMiddleware(
thunkMiddleware,
loggerMiddleware
),
window.devToolsExtension ? window.devToolsExtension() : f => f
)
);
export const history = syncHistoryWithStore(browserHistory, store)
export default store
ObjectList.js
import React from "react";
import ObjectItem from "../components/ObjectItem"
import Loader from "../components/Loader"
import fetchObjects from "../actions/actionCreators";
import switchUrl from "../actions/actionCreators";
import PaginationPanel from "../components/PaginationPanel"
import classNames from 'classnames'
import { push } from 'react-router-redux';
import { browserHistory } from 'react-router'
import store, { history } from '../store';
export default class ObjectList extends React.Component {
static defaultProps = {
objectsPerPage: 20,
objectContainerClassName: 'object_list_items'
}
constructor(props) {
super(props);
}
componentDidMount() {
this.props.fetchObjects(this.props.params.pageNum);
}
paginateHandler(page) {
this.props.history.push('/hotele/1/'+page)
}
render() {
const { resultsList } = this.props
if(resultsList.items.length > 0) {
const ObjectComponents = resultsList.items.map((item) => {
return <ObjectItem key={item.post_id} {...item}/>;
});
const paginationComponent =
<PaginationPanel
{...this.props}
pageNum={Math.ceil(resultsList.counters.allPosts/this.props.objectsPerPage)}
pageClickedHandler={this.paginateHandler.bind(this)}
currentPage={parseInt(this.props.params.pageNum)}
/>
return (
<div className="object-lists">
<div className={this.props.objectContainerClassName}>
<div>{ObjectComponents}</div>
</div>
{paginationComponent}
</div>
)
}
else if(!resultsList.isFetching || resultsList.items.length === 0) {
return <Loader />;
}
}
}
Home.js
import React from "react"
import { Link } from "react-router"
const Home = React.createClass({
render() {
return (
<div>
Strona główna <br />
<Link to={`/hotele/1/1`}>Lista wyszukiwania</Link>
</div>
)
}
})
export default Home
Results.js
import React from "react";
import ObjectList from "../components/ObjectList"
import CategoryTabs from "../components/CategoryTabs"
import fetchObjects from "../actions/actionCreators"
export default class Results extends React.Component{
constructor(props) {
super(props);
}
render() {
return (
<div>
<CategoryTabs { ...this.props } />
<ObjectList { ...this.props } />
</div>
);
}
}
reducers/index.js
import { combineReducers } from 'redux'
import { routerReducer } from 'react-router-redux'
import objects from './objects'
import categories from './categories'
const rootReducer = combineReducers({objects, categories, routing: routerReducer})
export default rootReducer
reducers/objects.js
function objects(state = {
isFetching: false,
items: [],
counters: []
}, action) {
switch (action.type) {
case 'RECEIVE_OBJECTS':
return Object.assign({}, state, {
isFetching: false,
items: action.objects.posts,
counters: action.objects.counters
})
default:
return state;
}
}
export default objects
app.js
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as actionCreators from '../actions/actionCreators';
import Main from '../components/Main';
function mapStateToProps(state) {
return {
resultsList: state.objects,
categories: state.categories
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(actionCreators, dispatch);
}
const App = connect(mapStateToProps, mapDispatchToProps)(Main);
export default App;
actionCreators.js
import fetch from 'isomorphic-fetch'
import { push } from 'react-router-redux';
function receiveObjects(objects, json) {
return {
type: 'RECEIVE_OBJECTS',
objects
}
}
function requestObject(pageNum) {
return {
type: 'REQUEST_OBJECTS',
pageNum
}
}
export function fetchObjects(pageNum) {
return dispatch => {
dispatch(requestObject(pageNum));
let url = 'http://localhost:8080/posts?city=986283&type=hotel&page='+pageNum;
return fetch(url)
.then(response => response.json())
.then(json => dispatch(receiveObjects(json)));
}
}
ObjectList component will not be mounted again because you are not changing components tree. It is still
<Home>
<Results>
<ObjectList />
</Results>
</Home>
It will be remounted only if you go to a different route and mount different root component so the whole tree would change. But You're just passing different props. You need to use
componentWillReceiveProps(nextProps) {
this.props.fetchObjects(nextProps.params.pageNum);
}