Updating this question to use connected-react-router instead of react-router-redux since it is not compatible with react-router v4.
I can't seem to get my routing working when dispatching an action. I suspect it's because I'm using sagas which aren't being configured properly.
I have a saga:
import { call } from 'redux-saga/effects'
import { push } from 'connected-react-router'
//...
yield call(push, '/dashboard')
The push function doesn't redirect the browser to the specified path despite the redux logs in webdev tools showing that the action was successfully dispatched.
The top level index.js file looks like:
import createSagaMiddleware from 'redux-saga'
import rootSaga from './redux/sagas'
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import logger from 'redux-logger'
import App from './App'
import registerServiceWorker from './registerServiceWorker'
import rootReducer from './redux/modules'
import { applyMiddleware, compose, createStore } from 'redux'
import { createBrowserHistory } from 'history'
import { routerMiddleware, connectRouter } from 'connected-react-router'
const history = createBrowserHistory()
const sagaMiddleware = createSagaMiddleware()
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(
connectRouter(history)(rootReducer),
composeEnhancer(
applyMiddleware(
sagaMiddleware,
routerMiddleware(history),
logger
)
)
)
sagaMiddleware.run(rootSaga)
const render = () => {
ReactDOM.render(
<Provider store={store}>
<App history={history} />
</Provider>,
document.getElementById('root')
)
}
render()
registerServiceWorker()
The App.js file containing the root component has:
import { ConnectedRouter } from 'connected-react-router'
import { Route, Switch, Redirect } from 'react-router-dom'
const App = ({ history }) => {
return (
<ConnectedRouter history={history}>
<Switch>
{ routes }
</Switch>
</ConnectedRouter>
)
}
export default App
What's missing from this setup to make it work?
Dependency versions:
"react-redux": "^5.0.7",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"connected-react-router": "^4.3.0"
Unlike history's push method (which is an impure function), connected-react-router's push is an action creator and its result (action) must be dispatched to trigger a navigation.
To do so in redux-saga you have to use put, not call.
call creates a call effect.
When yielded, it simply executes given function with given arguments and returns a result. It is a good fit for (but not limited by) impure function calls (e.g. network request), by decoupling us from a direct execution of a function.
put creates a dispatch effect.
When yielded, it dispatches passed in action object. Thus, decoupling your code only from a direct call of dispatch, not action creator (which should be pure by design).
So, in your case, the solution would look like:
yield put(push('/dashboard'))
P.S: the same applies to react-router-redux's push
you need to wire up the router's middleware, e.g.:
import { browserHistory } from 'react-router'
import { routerMiddleware } from 'react-router-redux'
const sagaMw = createSagaMiddleware()
const routerMw = routerMiddleware(browserHistory)
const middleware = applyMiddleware(sagaMw, routerMw, logger)
Sagas are implemented as Generator functions that yield objects to the
redux-saga middleware
So your Saga should export a Generator function:
import { call } from 'redux-saga/effects'
import { push } from 'connected-react-router'
//...
export function* rootSaga() {
return yield call(push, '/dashboard')
}
And rootSaga should be registered with the sagaMiddleware:
import { rootSaga } from './redux/sagas';
...
sagaMiddleware.run(rootSaga)
...
Reference: https://redux-saga.js.org/docs/introduction/BeginnerTutorial.html
What seems to work for me is to use withRoter (dont know if it is correct way or not):
export default withRouter(compose(withConnect)(App)); //wrapped in withRouter
Or
export default compose(
withRouter,
withConnect,
)(App);
And in redux-saga:
yield put(push('/newpage'));
I use react-boilerplate, https://github.com/react-boilerplate/react-boilerplate.
I don't know if it is correct or not but this way the route changes in url and I get to the new route. If i don't use withRouter the route changes in url and nothing more happens...
I found solution here https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/withRouter.md
Would like to have someone opinion on that. Folks at https://github.com/react-boilerplate/react-boilerplate maybe?
Related
I'm currently trying to use React + Typescript + Redux and I'm running into an issue. I'm trying to test the Redux Store setup via chrome devTools. I know I butchered the code (very new to Typescript) and I'm getting this error 'Uncaught TypeError: Cannot read properties of undefined (reading 'dispatch')' every time I test it. I tried declaring a global window state, installed redux-dev-tools, but still very lost.
This is what my store/index.tsx file look like:
import {
legacy_createStore as createStore,
combineReducers,
applyMiddleware,
compose,
StoreEnhancer,
} from "redux";
import { devToolsEnhancer } from "redux-devtools-extension";
import thunk from "redux-thunk";
const rootReducer = combineReducers({});
let enhancer;
if (process.env.NODE_ENV === "production") {
enhancer = applyMiddleware(thunk);
} else {
const logger = require("redux-logger").default;
const composeEnhancers =
(window && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;
enhancer = composeEnhancers(applyMiddleware(thunk, logger));
}
const configureStore = () => {
return createStore(rootReducer, devToolsEnhancer({}));
};
export default configureStore;
and my types/index.d.ts:
import { StoreEnhancer } from 'redux'
export {};
declare global {
interface Window {
store: {};
__REDUX_DEVTOOLS_EXTENSION__?: () => StoreEnhancer;
}
}
And finally my src/index.tsx:
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import configureStore from "./store";
const store = configureStore();
if (process.env.NODE_ENV !== "production") {
window.store = store;
};
function Root() {
return (
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
);
}
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<Root />
</React.StrictMode>
);
I will also attach screenshots of my file setup:
And my console error:
I am open to all suggestions, thank you!
The first issue here is that the store setup code is using outdated patterns and a lot of handwritten code. Today, you should be using our official Redux Toolkit package to write your Redux apps, and as part of that, RTK's configureStore API. It does all that same work with just a few lines:
import { configureStore } from "#reduxjs/toolkit";
const store = configureStore({
reducer: {
posts: postsReducer,
comments: commentsReducer
}
})
That automatically combines reducers, adds the Redux DevTools extension setup, and adds the thunk middleware.
See our docs for guidance on setup:
https://redux.js.org/tutorials/quick-start
https://redux.js.org/tutorials/typescript-quick-start
https://redux.js.org/introduction/why-rtk-is-redux-today
https://redux.js.org/tutorials/essentials/part-2-app-structure
As for the specific error message you're seeing... the code seems like it would run. My guess is that something is wrong with the process.env.NODE_ENV check you added and so it's not assigning window.store.
RTK also works much better with TypeScript than legacy Redux code does.
I am new to React.js and was setting up base project at that I was getting one issue that my routing got changed but component doesn't load. After googling I found that I need to use ConnectedRouter. While setting up ConnectedRouter, I am getting console error: Could not find router reducer in state tree, it must be mounted under "router"
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { ConnectedRouter, connectRouter, routerMiddleware } from "connected-react-router";
import { Provider } from "react-redux";
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import loginReducer from "./store/reducers/login";
import { watchLogin} from "./store/sagas";
import { history } from '../src/shared/history';
import { push } from 'react-router-redux';
import './index.css';
import App from './App';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const rootReducer = combineReducers({
login: loginReducer
});
const routersMiddleware = routerMiddleware(history)
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware, routersMiddleware];
const store = createStore(
connectRouter(history)(rootReducer),
{},
composeEnhancers(applyMiddleware(...middlewares))
);
sagaMiddleware.run(watchLogin);
const app = (
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>
);
ReactDOM.render(app, document.getElementById('root'));
For the sake of helping future souls with this issue, it turns out that according to the linked github discussions, that version 5.0 of the history package is causing the issue and downgrading to version 4.10.1 solves the problem for me.
npm install history#4.10.1
https://github.com/ReactTraining/history/issues/803
https://github.com/ReactTraining/history/issues/804
Add router in your reducer with using connectRouter and history
Refer this link
https://www.npmjs.com/package/connected-react-router
import { connectRouter } from 'connected-react-router'
const rootReducer = combineReducers({
login: loginReducer,
router: connectRouter(history),
});
The main issue is the version of the history package, with react-router-dom v5 you need to use history v4 (the latest version of which is 4.10.1) - history v5 is only compatible with react-router-dom v6.
You have forgotten :
router: connectRouter(history),
in your combineReducers()
If you use immutable you should import ConnectedRouter from 'connected-react-router/immutable'.
I ran into the same issue.
I forgot to give the history as a parameter to my rootReducer, in my store initialization.
const store = createStore(
rootReducer(history), // <-- HERE
{},
...
)
So I have been creating an application where a user needs to log into firebase using google authentication. I am using redux, react-redux, react-redux-firebase, redux-firestore, and redux-thunk. I am able to successfully log the user into firebase with the google authentication. I now want to use firestore in order to have a collection of all the users. I have looked at the documentation for redux-firestore and the method of getting/manipulating is a little different. I have tried using the documentation, but I cannot get the functions to work with redux-firestore.
Here is the action
export const signIn = () => (
dispatch,
getState,
{getFirebase, getFirestore}) => {
const firebase = getFirebase();
const firestore = getFirestore();
firebase.auth().signInWithPopup(provider).then(function(result) {
if(result.credential) {
firestore.get({collection: 'users', doc: result.user.uid}).then(function(doc) {
if(!doc.exists){
console.log("new!")
firestore.add(
{collection: 'users', doc: result.user.uid},
{name: firebase.auth.currentUser.displayName});
} else{
console.log("old!")
}
})
}
}).catch((err) => {
})
};
And here is my setup in index.js for the src folder
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import {BrowserRouter} from 'react-router-dom';
import {createStore, applyMiddleware, compose} from 'redux';
import {Provider} from 'react-redux';
import allReducers from './reducers';
import thunk from 'redux-thunk';
import firebase from './Firebase';
import {firebaseConfig} from './Firebase'
import {createFirestoreInstance, getFirestore, reduxFirestore} from 'redux-firestore';
import {ReactReduxFirebaseProvider, getFirebase} from 'react-redux-firebase';
const store = createStore(
allReducers,
compose(
applyMiddleware(thunk.withExtraArgument({getFirebase, getFirestore})),
reduxFirestore(firebaseConfig)
));
const rrfConfig = {
userProfile: 'users',
useFirestoreForProfile: true
};
const rrfProps = {
firebase,
config: rrfConfig,
dispatch: store.dispatch,
}
ReactDOM.render(
<Provider store={store}>
<ReactReduxFirebaseProvider {...rrfProps}>
<BrowserRouter>
<App />
</BrowserRouter>
</ReactReduxFirebaseProvider>
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();
I know that I have not used createFirestoreInstance in this code, but I was playing around with it.
If anyone could tell me how to get this working, I would appreciate it.
Thanks!
Quick update:
I have figured out how to at least write to firestore using this code
const userRef = firebase.firestore().collection('users').doc(result.user.uid);
userRef.get().then(function(doc) {
if(!doc.exists){
userRef.set({name: result.user.displayName});
}
})
This is not the best (or maybe the right solution), but it does work. It is not using redux-firestore, but is there a better way?
If you're using React, use react-redux-firebase. There's no need for these many complication and the code looks much neater and simpler. Authentication, firestore and all other firebase features works out of the box with just small amount of code. They also comes with React hooks like useFirebase() and useFirestore() instead of you needing to write them on your own.
react-redux-firebase is built on top of redux-firebase and provides all the things you would need in React.
If your app only uses firebase, I would even recommend you use just plain Redux without Redux Thunk or Redux Saga.
I am new to React.js and was setting up base project at that I was getting one issue that my routing got changed but component doesn't load. After googling I found that I need to use ConnectedRouter. While setting up ConnectedRouter, I am getting console error: Could not find router reducer in state tree, it must be mounted under "router"
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { ConnectedRouter, connectRouter, routerMiddleware } from "connected-react-router";
import { Provider } from "react-redux";
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import loginReducer from "./store/reducers/login";
import { watchLogin} from "./store/sagas";
import { history } from '../src/shared/history';
import { push } from 'react-router-redux';
import './index.css';
import App from './App';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const rootReducer = combineReducers({
login: loginReducer
});
const routersMiddleware = routerMiddleware(history)
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware, routersMiddleware];
const store = createStore(
connectRouter(history)(rootReducer),
{},
composeEnhancers(applyMiddleware(...middlewares))
);
sagaMiddleware.run(watchLogin);
const app = (
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>
);
ReactDOM.render(app, document.getElementById('root'));
For the sake of helping future souls with this issue, it turns out that according to the linked github discussions, that version 5.0 of the history package is causing the issue and downgrading to version 4.10.1 solves the problem for me.
npm install history#4.10.1
https://github.com/ReactTraining/history/issues/803
https://github.com/ReactTraining/history/issues/804
Add router in your reducer with using connectRouter and history
Refer this link
https://www.npmjs.com/package/connected-react-router
import { connectRouter } from 'connected-react-router'
const rootReducer = combineReducers({
login: loginReducer,
router: connectRouter(history),
});
The main issue is the version of the history package, with react-router-dom v5 you need to use history v4 (the latest version of which is 4.10.1) - history v5 is only compatible with react-router-dom v6.
You have forgotten :
router: connectRouter(history),
in your combineReducers()
If you use immutable you should import ConnectedRouter from 'connected-react-router/immutable'.
I ran into the same issue.
I forgot to give the history as a parameter to my rootReducer, in my store initialization.
const store = createStore(
rootReducer(history), // <-- HERE
{},
...
)
Redux advocates the use of a single store with a single state. However, with react-router you get multiple pages each having their own top-level root component.
How should one go about wiring up redux with a react-router app that has multiple pages? React-router 1.0 no longer lets you pass props to the routes so making the router the top level component that contains the state for all the pages is no longer possible nor is it feasible.
If you are using redux + react-router I would highly recommend using redux-router as well - this will allow you to keep route information in your store - I usually have the following setup.
redux-router: ^1.0.0-beta3 /
react-router": ^1.0.0-rc1 /
redux: "^3.0.2 /
react: "^0.14.0
//index.js [entry point]
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route } from 'react-router';
import { Provider } from 'react-redux';
import createStore from './utils/create-store';
import routes from './bootstrap/routes';
import { ReduxRouter } from 'redux-router';
const store = createStore(routes);
ReactDOM.render(
<Provider store={store}>
<ReduxRouter>
{routes}
</ReduxRouter>
</Provider>,
document.getElementById('root')
);
// create-store.js
import { applyMiddleware, createStore, combineReducers, compose } from 'redux';
import * as reducers from '../reducers/index';
import promiseMiddleware from './promise-middleware';
import { routerStateReducer, reduxReactRouter } from 'redux-router';
import history from './history'
/**
* Sets up the redux store. Responsible for loading up the reducers and middleware.
*
* #param routes
*/
export default function create(routes) {
const composedReducers = combineReducers({
router: routerStateReducer,
...reducers
});
const finalCreateStore = compose(applyMiddleware(promiseMiddleware),
reduxReactRouter({
routes,
history
}))(createStore);
let store = finalCreateStore(composedReducers);
return store;
}
// routes.js
import React from 'react';
import { Route } from 'react-router';
import App from './app';
module.exports = (
<Route component={App}>
...all your routes go here
</Route>
);
// app.js
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
export default class App extends React.Component {
render() {
const { props: { children } } = this;
return (
<div className="app-container">
{children}
</div>
);
}
}
So as you can see there is one higher order component that wraps the rest of your routes
Redux-router is no longer compatible with react-router v4, and the above solution will not work.
As such, I suggest utilzing redux-little-router. Unlike redux-router it is not dependent on react-router, but instead a replacement for it.
With it you can do things like:
Push to new routes from your async actions:
export function navigateAbout() {
return (dispatch) => {
dispatch(push('/about'))
}
}
See your router details (current path, querystring and params) inside redux-dev-tools, and access those details from you store like you would for any other prop:
function mapStateToProps(state) {
return {
string: state.router.query.string
}
}
PS. You can refer to or use this react boilerplate with redux-little-redux already implemented