Skip to content

Instantly share code, notes, and snippets.

@jasonswearingen
Last active October 31, 2017 19:37
Show Gist options
  • Save jasonswearingen/44068ebe04b1b077c8c9 to your computer and use it in GitHub Desktop.
Save jasonswearingen/44068ebe04b1b077c8c9 to your computer and use it in GitHub Desktop.
A simplified example of redux + redux-simple-router using Typescript
/**
WHAT: A very simple example of redux + redux-simple-router using Typescript.
WHY: The official example found here: https://github.com/rackt/redux-simple-router/tree/1.0.2/examples/basic has problems:
1) it is spread over many files making it very hard to "skim"
2) it is organized by function, not by feature. (Example: learning "how to manipulate redux state" is spread over 5 files in 4 folders)
3) there are no comments explaining what's going on/why.
WHO: by [email protected]
*/
////////////////// INSTALL THESE TYPINGS FROM https://github.com/DefinitelyTyped/DefinitelyTyped
import React = require("react");
import ReactDOM = require("react-dom");
import Redux = require("redux");
import ReactRedux = require("react-redux");
import ReactRouter = require("react-router"); //tested with 1.x, not sure if it works with 2.x
import History = require("history");
////////////////// imports that don't have DefinitelyTyped typings
/**
* //Needed for onTouchTap
//Can go away when react 1.0 release
//Check this repo:
//https://github.com/zilverline/react-tap-event-plugin
*/
var injectTapEventPlugin: Function = require("react-tap-event-plugin");
injectTapEventPlugin();
interface IReduxSimpleRouter {
/** Call this with a react-router and a redux store instance to install hooks that always keep both of them in sync. When one changes, so will the other.
Supply an optional function selectRouterState to customize where to find the router state on your app state. It defaults to state => state.routing, so you would install the reducer under the name "routing". Feel free to change this to whatever you like. */
syncReduxAndRouter(history: any, store: any, selectRouterState?: Function): void;
/** A reducer function that keeps track of the router state. You must to add this reducer to your app reducers when creating the store. If you do not provide a custom selectRouterState function, the piece of state must be named routing. */
routeReducer: Function;
/** An action type that you can listen for in your reducers to be notified of route updates. */
UPDATE_PATH: any;
/** An action creator that you can use to update the current URL and update the browser history. Just pass it a string like /foo/bar?param=5 as the path argument.
You can optionally pass a state to this action creator to update the state of the current route.
The avoidRouterUpdate, if true, will stop react-router from reacting to this URL change. This is useful if replaying snapshots while using the forceRefresh option of the browser history which forces full reloads. It's a rare edge case. */
pushPath(path: string, state: any, avoidRouterUpdate: boolean | {}): void;
/** An action creator that you can use to replace the current URL without updating the browser history.
The state and the avoidRouterUpdate parameters work just like pushPath. */
replacePath(path: string, state: any, avoidRouterUpdate: boolean | {}): void;
}
var ReduxSimpleRouter: IReduxSimpleRouter = require("redux-simple-router");
/**
* organize by feature: "count"
* this contains all logic for incrementing/decrementing a counter for use in redux (manipulate the redux state)
*/
module count {
module ActionType {
export const INCREASE = "INCREASE";
export const DECREASE = "DECREASE";
}
/**
* using a class instead of an interface so we can combine implementation with typing.
*
*/
export class IAction {
public increase(n: number): IActionResult<number> {
var action: IActionResult<number> = {
type: ActionType.INCREASE,
value: n,
}
return action;
}
public decrease(n: number): IActionResult<number> {
var action: IActionResult<number> = {
type: ActionType.DECREASE,
value: n,
}
return action;
}
}
/**
* an instance of our class, these are the "actions" that are executable by our React components.
* unfortunately you can't pass class-instances to redux, (the functions are not own-enumerable) so we need to use the prototype instead
*/
export var action: IAction = new IAction()["__proto__"];
const initialState = { number: 0 };
/**
* our reducer for applying the action to redux state.
* @param state
* @param action
*/
export function reducer(state = initialState, action: IActionResult<number>) {
if (action.type === ActionType.INCREASE) {
return { number: state.number + action.value };
} else if (action.type === ActionType.DECREASE) {
return { number: state.number - action.value };
}
return state;
}
}
/** the output of all redux actions should be in this form */
interface IActionResult<T> {
type: string; //required by redux
value: T; // our opinionated encapsulation of state changes
}
module components {
interface _AppProps extends ReactRouter.RouteComponentProps<{}, { id: number }> {
pushPath: (path: string, state?: any, avoidRouterUpdate?: boolean | {}) => void;
}
class _App extends React.Component<_AppProps, {}>{
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div>
<header>
Links:
{' '}
<ReactRouter.Link to="/">Home</ReactRouter.Link>
{' '}
<ReactRouter.Link to="/foo">Foo</ReactRouter.Link>
{' '}
<ReactRouter.Link to="/bar">Bar</ReactRouter.Link>
</header>
<div>
<button onClick={() => this.props.pushPath('/foo') }>Go to /foo</button>
</div>
<div style={{ marginTop: '1.5em' }}>{this.props.children}</div>
</div>
);
}
}
/**
* a connected instance of the App component (bound to redux)
*/
export var App: typeof _App = ReactRedux.connect(null, { pushPath: ReduxSimpleRouter.pushPath })(_App); //redux-binds, then includes the pushPath() method for use in our _App Component
export class Bar extends React.Component<{}, {}>{
render() {
return (<div>And I am Bar!</div>);
}
}
export class Foo extends React.Component<{}, {}>{
render() {
return (<div>And I am Foo!</div>);
}
}
interface _HomeProps extends ReactRouter.RouteComponentProps<{}, { id: number }> {
number: number;
}
class _Home extends React.Component<_HomeProps & count.IAction, {}>{ //note: the "&" type operator was added in Typescript 1.6, allowing intersection of types (mixins).
render() {
return (
<div>
Some state changes:
{this.props.number}
<button onClick={
() => { this.props.increase(1); } //the suggested way to execute actions is via bound props like this.
}>Increase</button>
<button onClick={
() => boundActions.decrease(1) //but another way of doing it is via Redux.bindActionCreators
}>Decrease</button>
</div>
);
}
}
/**
* a connected instance of the Home component (bound to redux)
*/
export var Home: typeof _Home = ReactRedux.connect(
(reduxState) => { //subscribes to reduxStore updates. this method is called every time an update occurs.
return { number: reduxState.count.number }; //map reduxStoreState to a property you use in the _Home component
},
count.action as any //redux-binds, then include our count actions as properties. using Typescript, we have to cast as any for our class type signature to work
)(_Home);
}
/**
* workflow for redux devtools v3 https://github.com/gaearon/redux-devtools
*/
module reduxDevTools {
/////////////////////////////// REDUX DEVTOOLS v3
const ReduxDevTools3 = require("redux-devtools");
const createDevTools = ReduxDevTools3.createDevTools;
// Monitors are separate packages, and you can make a custom one
const LogMonitor = require("redux-devtools-log-monitor").default; //need to use ".default" because of es6 module structure
const DockMonitor = require("redux-devtools-dock-monitor").default;
// createDevTools takes a monitor and produces a DevTools component
export const DevTools: {
/** pass as the last argument to Redux.compose() */
instrument(): typeof Redux.createStore;
} = createDevTools(
// Monitors are individually adjustable with props.
// Consult their repositories to learn about those props.
// Here, we put LogMonitor inside a DockMonitor.
<DockMonitor toggleVisibilityKey='ctrl-h'
changePositionKey='ctrl-q'>
<LogMonitor theme='tomorrow' />
</DockMonitor>
);
}
/**
* //create a single reducer function from our components
*/
const reducer = Redux.combineReducers({
count: count.reducer, //our example 'count' reducer
routing: ReduxSimpleRouter.routeReducer //include our routing module
});
/**
* //binds "apply middleware" (the devTools) to our store
*/
const finalCreateStore = Redux.compose(reduxDevTools.DevTools.instrument())(Redux.createStore)
/**
* the whole state tree of the app use actions to manipulate
*/
const reduxStore: Redux.Store = finalCreateStore(reducer);
const history = History.createHashHistory(); //use hash based history
ReduxSimpleRouter.syncReduxAndRouter(history, reduxStore);
/**
* an example of how to bind actions to redux without going through bound-components
*/
var boundActions = Redux.bindActionCreators(count.action, reduxStore.dispatch);
///////////// render to the page, a-la normal React.
ReactDOM.render(
<ReactRedux.Provider store={reduxStore}>
<div>
<ReactRouter.Router history={history}>
<ReactRouter.Route path="/" component={components.App}>
<ReactRouter.IndexRoute component={components.Home}/>
<ReactRouter.Route path="foo" component={components.Foo}/>
<ReactRouter.Route path="bar" component={components.Bar}/>
</ReactRouter.Route>
</ReactRouter.Router>
<reduxDevTools.DevTools/>
</div>
</ReactRedux.Provider>,
document.getElementById('react-mount') //make sure a dom-element with this id exists on the page
);
@jasonswearingen
Copy link
Author

changed from redux-devtools v2.x to v3.x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment