How to organize state with redux and react-router? - reactjs

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

Related

Performing a condition based on the current URL with react.js and react router v6

Using React.js & react router v6
Is there a way I can extract information from the current URL? Let's say my current URL is this: localhost:3000/about/x567gfh67ssd90g
I simply want to perform a condition based on the current URL. Is there any possible way of doing this?
import { useLocation } from "react-router-dom";
const Component = () => {
const location = useLocation();
useEffect(() => {
console.log(location);
});
return ();
};
You can get pathname using useLocation.
You can also add connected-react-router to your project. This way you will have access to all the current route information in your redux-store. This will also allow you to "Time travel" through your SPA. When you hit the backbutton instead of going to the previous page you can go back through state.
import React from 'react';
import { render } from 'react-dom';
import { Provider, ReactReduxContext } from 'react-redux';
import { ConnectedRouter } from "connected-react-router";
import { store, history } from './Store/store';
import { App } from './App/App';
render(
<Provider store={ store } context={ ReactReduxContext } >
<ConnectedRouter context={ ReactReduxContext } history={ history }>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('app')
);
If you install the redux-devTools in your browser you can look at all the URL data like this- Obviously this is a little more advanced but you it will give you access to everything you need.

Redux TS Generic type 'Dispatch<S>' requires 1 type argument(s)

Trying to setup a project with typescript and redux.
I am getting this error
Generic type 'Dispatch<S>' requires 1 type argument(s).
here is my store.ts
import { connectRouter, routerMiddleware } from 'connected-react-router'
import { applyMiddleware, compose, createStore } from 'redux'
import { createLogger } from 'redux-logger'
import ReduxThunk from 'redux-thunk'
import { createBrowserHistory } from 'history'
import reducers from './reducers'
import { composeWithDevTools } from 'redux-devtools-extension'
export const history = createBrowserHistory()
const composeEnhancers = composeWithDevTools({
// Specify name here, actionsBlacklist, actionsCreators and other options if needed
})
const logger = createLogger()
const middleware = [ReduxThunk, logger]
const Store = createStore(connectRouter(history)(reducers), {}, composeEnhancers(applyMiddleware(...middleware, routerMiddleware(history))))
export default Store
here is root reducer
import { combineReducers } from 'redux'
import { ActionType } from 'typesafe-actions'
import * as actions from '../actions'
export interface IState {
test: string
}
export type Actions = ActionType<typeof actions>
export default combineReducers<IState, Actions>({
test: () => 'hey'
})
and here are some dummy actions
import { action } from 'typesafe-actions'
export const toggle = (id: string) => action('TOGGLE', id)
// (id: string) => { type: 'todos/TOGGLE'; payload: string; }
finally here is index.ts
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import App from './App'
import './index.scss'
import registerServiceWorker from './registerServiceWorker'
import store, { history } from './store'
import { Provider } from 'react-redux'
import { Route, Switch } from 'react-router' // react-router v4
import { ConnectedRouter } from 'connected-react-router'
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}> { /* place ConnectedRouter under Provider */}
<div> { /* your usual react-router v4 routing */}
<Switch>
<Route exact path="/" render={() => (<div>Match</div>)} />
<Route render={() => (<div>Miss</div>)} />
</Switch>
</div>
</ConnectedRouter>
</Provider>,
document.getElementById('root') as HTMLElement
)
registerServiceWorker()
Here seems to be a similar issue without solution yet
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/9611
But I am new to typescript so might be missing something basic
It looks to me like you are indeed facing the same issue you linked. While we wait and see if 7mllm7's pull request is merged, you can use his modified version of the react-redux types. I'd recommend the following approach:
git clone --depth=1 https://github.com/7mllm7/DefinitelyTyped
Copy the types/react-redux folder into your project (suppose for example you copy it to a folder named react-redux.fixed).
Edit react-redux.fixed/package.json to replace "private": "true" with "name": "#types/react-redux".
In your package.json, specify the version of #types/react-redux as ./react-redux.fixed.
Run npm install. npm will make a symlink from node_modules/#types/react-redux to react-redux.fixed.
Compared to just editing the file in node_modules/#types/react-redux, this way npm knows you are using a modified version of the package and won't overwrite it. (This process deserves to be widely known; I'll find a better place to document it if I have time.)
I solved this by downgrading to Redux 3.7. It has proper typings (There still aren't typings for Redux 4.0). There are some Github issues where they discuss about it (here and here).

React redux is not changing routes

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?

How to access react-router-v4 history prop from MobX store

Is there any way to create an action in your MobX store, which pushes your app to a new url using react router v4, e.g. this.props.history.push...
I constantly get a history undefined error, but am unsure how to access the history from my store.
The history push called from the component itself does work though..
many thanks! (this is driving me crazy..)
Since I stumbled across the same issue, I'll share my solution. I just put the RouterStore into its own file in my stores directory, then if I needed access to history or location or whatever, I would import the routing store into the store I was currently working in.
./stores/routing.ts
import { RouterStore } from 'mobx-react-router'
export default new RouterStore()
./stores/other-store.ts
import routing from './routing'
export class OtherStore {
#action
doSomething = () => {
routing.push('/new-route')
}
}
export default new OtherStore()
./index.ts
import { Router } from 'react-router-dom'
import { Provider } from 'mobx-react'
import createBrowserHistory from 'history/createBrowserHistory'
import { syncHistoryWithStore } from 'mobx-react-router'
import otherStore from './stores/other-store'
import routing from './stores/routing'
const browserHistory = createBrowserHistory()
const stores = {
otherStore,
routing,
}
const history = syncHistoryWithStore(browserHistory, routing)
ReactDOM.render(
<Provider {...stores}>
<Router history={history}>
<App />
</Router>
</Provider>,
document.getElementById('root'),
)
You can use mobx-react-router to put react-router in a mobx store and then use it by injecting it in components.
You can also pass the router store as a constructor argument to your other stores that need it. This way you have the router history instance available in your mobx store.
I would like to add a simpler solution that does not require any additional libraries. React Router version is 5.2
Among my stores i've created a HistoryStore.js with the following code:
import { createBrowserHistory } from 'history';
export class HistoryStore {
history = createBrowserHistory();
}
Then I create an instance of it in my contexts.js file but you could do it right away.
export const history = new HistoryStore();
After that you import it in your index.js and pass it as a history prop to the Router.
That's it. Now you could import this store into any other and use it there. When you use useHistory hook in your component it gets this history object, so your history in synchronized.

redux react and socket.io

I somehow managed to get it work, but i think it is not good solution.
my main file is:
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, hashHistory } from 'react-router';
import { Provider } from 'react-redux';
import routes from './router/routes';
import store from './store.js';
ReactDOM.render(
<Provider store={ store }>
<Router routes={ routes } history={ hashHistory } />
</Provider>,
document.getElementById('wrap')
);
my store.js is:
import { createStore } from 'redux';
import reducer from './reducers/index';
import initialState from './reducers/initial-state';
import connection from './lib/connection';
const store = createStore(reducer, initialState);
connection(store);
export default store;
and connection is:
import AppActions from '../actions/AppActions';
import io from 'socket.io-client';
export default function (store) {
const mainsocket = io.connect('$socketurl');
mainsocket.store = store;
mainsocket.actions = AppActions;
var socket;
mainsocket.on('namespaceinitialized', function(data) {
socket = io.connect("$socketurl/$namespace", {'autoConnect': false,'forceNew': true});
socket.connect();
socket.store = store;
socket.actions = AppActions;
socket.on('connect',function (data) {
//need store and actions
});
});
}
As you see i am using 2 connections, first for namespace initialize and then second (this is not important in question).
I dont like this "dirty" solution where i am attaching store and actions to mainsocket object and then passing them from mainsocket to socket, in order to have access to app actions on socket messages, and the question is what is right way?
Also in this code socket connects before app actions and store have initialized, i would like to socket connection be last thing but i don't know where to put it then?
I must say that i am don't writing my own application but adding functionality to existing open source one.
thank you.

Resources