react-redux-saga-router
Advanced tools
Comparing version 0.6.1 to 0.6.2
@@ -119,2 +119,3 @@ 'use strict'; | ||
// eslint-disable-line | ||
var useProps = _extends({}, props); | ||
@@ -162,3 +163,3 @@ var map = ['component', 'loadingComponent', 'else']; | ||
Toggle.propTypes = (_Toggle$propTypes = {}, _defineProperty(_Toggle$propTypes, names.component, _react.PropTypes.oneOfType([_react.PropTypes.element, _react.PropTypes.func])), _defineProperty(_Toggle$propTypes, names.loadingComponent, _react.PropTypes.oneOfType([_react.PropTypes.element, _react.PropTypes.func])), _defineProperty(_Toggle$propTypes, 'loading', function loading(props, propName, componentName) { | ||
if (!Object.hasOwnProperty.call(props, 'loadingComponent')) { | ||
if (Object.hasOwnProperty.call(props, 'loading') && !Object.hasOwnProperty.call(props, 'loadingComponent')) { | ||
if (!componentLoadingMap.loadingComponent) { | ||
@@ -165,0 +166,0 @@ console.warn(propName + ' in ' + componentName + ' should be loadingComponent for react-redux-saga-router'); |
{ | ||
"name": "react-redux-saga-router", | ||
"version": "0.6.1", | ||
"version": "0.6.2", | ||
"description": "elegant powerful routing based on the simplicity of storing url as state", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
198
README.md
@@ -17,3 +17,5 @@ # react-redux-saga-router | ||
* [Simple example](#simple-example) | ||
* [Internal Linking with <Link>](#internal-linking-with-link) | ||
* [Extending the example: asynchronous state loading](#extending-the-example-asynchronous-state-loading) | ||
* [Available selectors for Toggle](#available-selectors-for-toggle) | ||
* [What about complex routes like react\-router <Route>?](#what-about-complex-routes-like-react-router-route) | ||
@@ -25,2 +27,3 @@ * [Dynamic Routes](#dynamic-routes) | ||
* [Reverse routing: creating URLs from parameters](#reverse-routing-creating-urls-from-parameters) | ||
* [Why a new router?](#why-a-new-router) | ||
* [Principles](#principles) | ||
@@ -36,3 +39,2 @@ * [URL state is just another asynchronous input to redux state](#url-state-is-just-another-asynchronous-input-to-redux-state) | ||
## Simple example | ||
@@ -292,2 +294,83 @@ | ||
### Available selectors for Toggles | ||
The following selectors are available for use with Toggles. import as follows: | ||
```javascript | ||
import * as selectors from 'react-redux-saga-router/selectors' | ||
``` | ||
#### matchedRoute(state, name) | ||
```javascript | ||
import * as selectors from 'react-redux-saga-router/selectors' | ||
import Toggle from 'react-redux-saga-router/Toggle' | ||
export Toggle(state => selectors.matchedRoute(state, 'routename')) | ||
``` | ||
This selector returns true if the route specified by `'routename'` is active | ||
#### noMatches | ||
```javascript | ||
import * as selectors from 'react-redux-saga-router/selectors' | ||
import Toggle from 'react-redux-saga-router/Toggle' | ||
export Toggle(state => selectors.noMatches(state)) | ||
``` | ||
This selector returns true if no routes match, and can be used for an error component | ||
or default component | ||
#### stateExists | ||
```javascript | ||
import * as selectors from 'react-redux-saga-router/selectors' | ||
import Toggle from 'react-redux-saga-router/Toggle' | ||
export Toggle(state => state.whatever, state => selectors.stateExists(state, /* state descriptor */)) | ||
``` | ||
This toggle is designed to be used to detect whether state has loaded. Pass in | ||
a skeleton of the state shape and it will traverse the state to determine whether it exists. | ||
Here is a sample from an actual project: | ||
```javascript | ||
import Toggle from 'react-redux-saga-router/Toggle' | ||
import * as selectors from 'react-redux-saga-router/selectors' | ||
export const check = state => selectors.stateExists(state, { | ||
campers: { | ||
ids: [] | ||
}, | ||
groups: { | ||
ids: [], | ||
groups: {}, | ||
selectedGroup: (group, state) => { | ||
if (!group) return true | ||
if (state.groups.ids.indexOf(group) === -1) return false | ||
const g = state.groups.groups[group] | ||
if (!g) return false | ||
if (g.type && !state.ensembleTypes.ensembleTypes[g.type]) return false | ||
if (g.members.length) { | ||
if (g.members.some(m => m ? !state.campers.campers[m] : false)) return false | ||
} | ||
return true | ||
} | ||
}, | ||
ensembleTypes: { | ||
ids: [], | ||
}, | ||
}) | ||
export default Toggle(state => state.groups.selectedGroup, check) | ||
``` | ||
The selector verifies that the campers and ensembleTypes state areas have an ids | ||
member that is an array, and that the groups state area has ids and groups set up. | ||
For selectedGroup, a callback is called, passed the value of the state item plus the | ||
entire state tree. The callback verifies that the selected group's state is internally | ||
consistent and when everything is set up, returns true. | ||
### What about complex routes like react-router `<Route>`? | ||
@@ -503,2 +586,115 @@ | ||
## Why a new router? | ||
[react-router](https://github.com/ReactTraining/react-router) is a mature router for | ||
React that has a huge following and community support. Why create a new router? | ||
In my work with react-router, I found that it was not possible to achieve | ||
some basic goals using react-router. I couldn't figure out a way to store state from | ||
url parameters and easily change the url from the state when using redux. It is the | ||
classic two-way binding issue: if there are 2 sources of state, they will fight and | ||
cause unexpected bugs. | ||
In addition, I moved to redux for state because the tree of components in React rarely | ||
corresponds to the way data is used. In many cases, I find myself rendering different | ||
portions of the component tree using the same data. So I will have 2 React components | ||
in totally different parts of the component tree using the same piece of data. | ||
With react-router, I found myself duplicating a lot of content with a single component, | ||
or using complex routing rules to enable displaying this information. | ||
With react-redux-saga-router, multiple components can respond to a route change anywhere | ||
in the React component tree, allowing for more modular design. It is important to note | ||
here that react-router version 4 addresses this design flaw by using a similar design | ||
principle to react-redux-saga-router. Great minds think alike. However, by coupling | ||
tightly the route definition and the component display, you are still limited to using | ||
a single component per route, and so the ability to display different components for | ||
the same route would require using some clever hacks such as coupling both components into | ||
a single component that chooses which one to render based on the route params passed in | ||
from react-router. The below solution is more performant both because the components | ||
are not rendered at all if the route is not satisfied. | ||
```javascript | ||
import React from 'react' | ||
import Routes from 'react-redux-saga-router/Routes' | ||
import Route from 'react-redux-saga-router/Route' | ||
import Toggle from 'react-redux-saga-router/Toggle' | ||
import { connect } from 'react-redux' | ||
import * as actions from './actions' | ||
const albumRouteMapping = { | ||
stateFromParams: params => ({ id: params.album, track: +params.track }), | ||
paramsFromState: state => ({ | ||
album: state.albums.selectedAlbum.id, | ||
track: state.albums.selectedTrack ? state.albums.selectedTrack : undefined | ||
}), | ||
updateState: { | ||
id: id => actions.selectAlbum(id), | ||
track: track => actions.playTrack(track) | ||
} | ||
} | ||
const TrackToggle = Toggle(state => state.albums.selectedTrack, | ||
state => state.albums.selectedTrack | ||
&& state.albums.allTracks[state.albums.selectedTrack].loading) | ||
const AlbumToggle = Toggle(state => state.albums.selectedAlbum) | ||
const AlbumList = ({ albums, selectAlbum }) => ( | ||
<ul> | ||
{albums.map(album => <li key={album.id} onClick={selectAlbum(album.id)}>{album.name}</li>)} | ||
</ul> | ||
) | ||
const AlbumDetail = ({ album, playTrack }) => ( | ||
<ul> | ||
<li>Album details</li> | ||
<li>...(stuff from the {album.name}</li> | ||
{album.tracks.map(track => <li key={track.id} onClick={playTrack(track.id)}>{track.name}</li>)} | ||
</ul> | ||
) | ||
const AlbumSummary = ({ album }) => { | ||
<h1> | ||
{album.name} | ||
</h1> | ||
} | ||
const TrackPlayer = ({ track }) => { | ||
<div> | ||
<h1>{track.title}</h1> | ||
<AudioPlayer audio={track.audio} /> <!-- pretend this exists --> | ||
</div> | ||
} | ||
const AlbumSummaryContainer = connect(state => ({ album: state.albums.selectedAlbum }))(AlbumSummary) | ||
const AlbumListContainer = connect(state => ({ albums: state.albums.allAlbums }), | ||
dispatch => ({ selectAlbum: id => dispatch(actions.selectAlbum(id)) }))(AlbumList) | ||
const AlbumDetailContainer = connect(state => ({ album: state.albums.selectedAlbum }), | ||
dispatch => ({ playTrack: id => dispatch(actions.selectTrack(id)) }))(AlbumDetail) | ||
const TrackPlayerContainer = connect(state => ({ track: state.albums.tracks[state.albums.selectedTrack] }))(TrackPlayer) | ||
const MyComponent = () => ( | ||
<div> | ||
<Routes> | ||
<Route name="main" path="/" /> | ||
<Route name="albumlist" path="/albums(/album/:album(/track/:track))" | ||
{...albumRouteMapping} | ||
/> | ||
</Routes> | ||
<TwoPanelLayout> | ||
<Panel> | ||
<AlbumToggle component={AlbumSummaryContainer} /> | ||
<TrackToggle component={TrackPlayerContainer} /> | ||
</Panel> | ||
<Panel> | ||
<AlbumToggle component={AlbumDetailContainer} else={AlbumListContainer} /> | ||
</Panel> | ||
</TwoPanelLayout> | ||
</div> | ||
) | ||
``` | ||
In addition, declaring new routes in asynchronously loaded code is trivial with this | ||
design. One need only put in `<Routes>` declarations in the child code and the new routes | ||
will be added. | ||
## Principles | ||
@@ -505,0 +701,0 @@ |
@@ -67,4 +67,4 @@ import React, { PropTypes } from 'react' | ||
function Toggle({ component: Component = defaults.component, else: ElseComponent = defaults.else, | ||
loadingComponent: Loading = defaults.loadingComponent, children, ...props }) { | ||
function Toggle({ component: Component = defaults.component, else: ElseComponent = defaults.else, // eslint-disable-line | ||
loadingComponent: Loading = defaults.loadingComponent, children, ...props }) { // eslint-disable-line | ||
const useProps = { ...props } | ||
@@ -114,3 +114,3 @@ const map = ['component', 'loadingComponent', 'else'] | ||
loading: (props, propName, componentName) => { | ||
if (!Object.hasOwnProperty.call(props, 'loadingComponent')) { | ||
if (Object.hasOwnProperty.call(props, 'loading') && !Object.hasOwnProperty.call(props, 'loadingComponent')) { | ||
if (!componentLoadingMap.loadingComponent) { | ||
@@ -117,0 +117,0 @@ console.warn(`${propName} in ${componentName} should be loadingComponent for react-redux-saga-router`) |
108455
1966
795