Skip to content
This repository was archived by the owner on Apr 8, 2020. It is now read-only.

Commit 9b048c5

Browse files
Remove 'redux-typed' from ReactReduxSpa template, making it more standard as a Redux application
1 parent cdd6c16 commit 9b048c5

File tree

10 files changed

+89
-69
lines changed

10 files changed

+89
-69
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import * as React from 'react';
22
import { Link } from 'react-router';
3-
import { provide } from 'redux-typed';
3+
import { connect } from 'react-redux';
44
import { ApplicationState } from '../store';
55
import * as CounterStore from '../store/Counter';
6+
import * as WeatherForecasts from '../store/WeatherForecasts';
7+
8+
type CounterProps = CounterStore.CounterState & typeof CounterStore.actionCreators;
69

710
class Counter extends React.Component<CounterProps, void> {
811
public render() {
@@ -18,10 +21,8 @@ class Counter extends React.Component<CounterProps, void> {
1821
}
1922
}
2023

21-
// Build the CounterProps type, which allows the component to be strongly typed
22-
const provider = provide(
23-
(state: ApplicationState) => state.counter, // Select which part of global state maps to this component
24-
CounterStore.actionCreators // Select which action creators should be exposed to this component
25-
);
26-
type CounterProps = typeof provider.allProps;
27-
export default provider.connect(Counter);
24+
// Wire up the React component to the Redux store
25+
export default connect(
26+
(state: ApplicationState) => state.counter, // Selects which state properties are merged into the component's props
27+
CounterStore.actionCreators // Selects which action creators are merged into the component's props
28+
)(Counter);

templates/ReactReduxSpa/ClientApp/components/FetchData.tsx

+13-14
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import * as React from 'react';
22
import { Link } from 'react-router';
3-
import { provide } from 'redux-typed';
3+
import { connect } from 'react-redux';
44
import { ApplicationState } from '../store';
55
import * as WeatherForecastsState from '../store/WeatherForecasts';
66

7-
interface RouteParams {
8-
startDateIndex: string;
9-
}
7+
// At runtime, Redux will merge together...
8+
type WeatherForecastProps =
9+
WeatherForecastsState.WeatherForecastsState // ... state we've requested from the Redux store
10+
& typeof WeatherForecastsState.actionCreators // ... plus action creators we've requested
11+
& { params: { startDateIndex: string } }; // ... plus incoming routing parameters
1012

1113
class FetchData extends React.Component<WeatherForecastProps, void> {
1214
componentWillMount() {
13-
// This method runs when the component is first added to the page
15+
// This method runs when the component is first added to the page
1416
let startDateIndex = parseInt(this.props.params.startDateIndex) || 0;
1517
this.props.requestWeatherForecasts(startDateIndex);
1618
}
17-
19+
1820
componentWillReceiveProps(nextProps: WeatherForecastProps) {
1921
// This method runs when incoming props (e.g., route params) change
2022
let startDateIndex = parseInt(nextProps.params.startDateIndex) || 0;
@@ -52,7 +54,7 @@ class FetchData extends React.Component<WeatherForecastProps, void> {
5254
</tbody>
5355
</table>;
5456
}
55-
57+
5658
private renderPagination() {
5759
let prevStartDateIndex = this.props.startDateIndex - 5;
5860
let nextStartDateIndex = this.props.startDateIndex + 5;
@@ -65,10 +67,7 @@ class FetchData extends React.Component<WeatherForecastProps, void> {
6567
}
6668
}
6769

68-
// Build the WeatherForecastProps type, which allows the component to be strongly typed
69-
const provider = provide(
70-
(state: ApplicationState) => state.weatherForecasts, // Select which part of global state maps to this component
71-
WeatherForecastsState.actionCreators // Select which action creators should be exposed to this component
72-
).withExternalProps<{ params: RouteParams }>(); // Also include a 'params' property on WeatherForecastProps
73-
type WeatherForecastProps = typeof provider.allProps;
74-
export default provider.connect(FetchData);
70+
export default connect(
71+
(state: ApplicationState) => state.weatherForecasts, // Selects which state properties are merged into the component's props
72+
WeatherForecastsState.actionCreators // Selects which action creators are merged into the component's props
73+
)(FetchData);

templates/ReactReduxSpa/ClientApp/components/Home.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22

3-
export default class Home extends React.Component<any, void> {
3+
export default class Home extends React.Component<void, void> {
44
public render() {
55
return <div>
66
<h1>Hello, world!</h1>

templates/ReactReduxSpa/ClientApp/components/NavMenu.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import { Link } from 'react-router';
33

4-
export class NavMenu extends React.Component<any, void> {
4+
export class NavMenu extends React.Component<void, void> {
55
public render() {
66
return <div className='main-nav'>
77
<div className='navbar navbar-inverse'>

templates/ReactReduxSpa/ClientApp/configureStore.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ import { createStore, applyMiddleware, compose, combineReducers, GenericStoreEnh
22
import thunk from 'redux-thunk';
33
import { routerReducer } from 'react-router-redux';
44
import * as Store from './store';
5-
import { typedToPlain } from 'redux-typed';
65

76
export default function configureStore(initialState?: Store.ApplicationState) {
87
// Build middleware. These are functions that can process the actions before they reach the store.
98
const windowIfDefined = typeof window === 'undefined' ? null : window as any;
109
// If devTools is installed, connect to it
1110
const devToolsExtension = windowIfDefined && windowIfDefined.devToolsExtension as () => GenericStoreEnhancer;
1211
const createStoreWithMiddleware = compose(
13-
applyMiddleware(thunk, typedToPlain),
12+
applyMiddleware(thunk),
1413
devToolsExtension ? devToolsExtension() : f => f
1514
)(createStore);
1615

templates/ReactReduxSpa/ClientApp/store/Counter.ts

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { typeName, isActionType, Action, Reducer } from 'redux-typed';
2-
import { ActionCreator } from './';
1+
import { Action, Reducer, ThunkAction } from 'redux';
32

43
// -----------------
54
// STATE - This defines the type of data maintained in the Redux store.
@@ -13,25 +12,34 @@ export interface CounterState {
1312
// They do not themselves have any side-effects; they just describe something that is going to happen.
1413
// Use @typeName and isActionType for type detection that works even after serialization/deserialization.
1514

16-
@typeName("INCREMENT_COUNT")
17-
class IncrementCount extends Action {
18-
}
15+
interface IncrementCountAction { type: 'INCREMENT_COUNT' }
16+
interface DecrementCountAction { type: 'DECREMENT_COUNT' }
17+
18+
// Declare a 'discriminated union' type. This guarantees that all references to 'type' properties contain one of the
19+
// declared type strings (and not any other arbitrary string).
20+
type KnownAction = IncrementCountAction | DecrementCountAction;
1921

2022
// ----------------
2123
// ACTION CREATORS - These are functions exposed to UI components that will trigger a state transition.
2224
// They don't directly mutate state, but they can have external side-effects (such as loading data).
2325

2426
export const actionCreators = {
25-
increment: (): ActionCreator => (dispatch, getState) => {
26-
dispatch(new IncrementCount());
27-
}
27+
increment: () => <IncrementCountAction>{ type: 'INCREMENT_COUNT' },
28+
decrement: () => <DecrementCountAction>{ type: 'DECREMENT_COUNT' }
2829
};
2930

3031
// ----------------
3132
// REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state.
32-
export const reducer: Reducer<CounterState> = (state, action) => {
33-
if (isActionType(action, IncrementCount)) {
34-
return { count: state.count + 1 };
33+
34+
export const reducer: Reducer<CounterState> = (state: CounterState, action: KnownAction) => {
35+
switch (action.type) {
36+
case 'INCREMENT_COUNT':
37+
return { count: state.count + 1 };
38+
case 'DECREMENT_COUNT':
39+
return { count: state.count - 1 };
40+
default:
41+
// The following line guarantees that every action in the KnownAction union has been covered by a case above
42+
const exhaustiveCheck: never = action;
3543
}
3644

3745
// For unrecognized actions (or in cases where actions have no effect), must return the existing state
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { fetch, addTask } from 'domain-task';
2-
import { typeName, isActionType, Action, Reducer } from 'redux-typed';
3-
import { ActionCreator } from './';
2+
import { Action, Reducer, ThunkAction, ActionCreator } from 'redux';
3+
import { AppThunkAction } from './';
44

55
// -----------------
66
// STATE - This defines the type of data maintained in the Redux store.
@@ -21,57 +21,70 @@ export interface WeatherForecast {
2121
// -----------------
2222
// ACTIONS - These are serializable (hence replayable) descriptions of state transitions.
2323
// They do not themselves have any side-effects; they just describe something that is going to happen.
24-
// Use @typeName and isActionType for type detection that works even after serialization/deserialization.
2524

26-
@typeName("REQUEST_WEATHER_FORECASTS")
27-
class RequestWeatherForecasts extends Action {
28-
constructor(public startDateIndex: number) {
29-
super();
30-
}
25+
interface RequestWeatherForecastsAction {
26+
type: 'REQUEST_WEATHER_FORECASTS',
27+
startDateIndex: number;
3128
}
3229

33-
@typeName("RECEIVE_WEATHER_FORECASTS")
34-
class ReceiveWeatherForecasts extends Action {
35-
constructor(public startDateIndex: number, public forecasts: WeatherForecast[]) {
36-
super();
37-
}
30+
interface ReceiveWeatherForecastsAction {
31+
type: 'RECEIVE_WEATHER_FORECASTS',
32+
startDateIndex: number;
33+
forecasts: WeatherForecast[]
3834
}
3935

36+
// Declare a 'discriminated union' type. This guarantees that all references to 'type' properties contain one of the
37+
// declared type strings (and not any other arbitrary string).
38+
type KnownAction = RequestWeatherForecastsAction | ReceiveWeatherForecastsAction;
39+
4040
// ----------------
4141
// ACTION CREATORS - These are functions exposed to UI components that will trigger a state transition.
4242
// They don't directly mutate state, but they can have external side-effects (such as loading data).
4343

4444
export const actionCreators = {
45-
requestWeatherForecasts: (startDateIndex: number): ActionCreator => (dispatch, getState) => {
45+
requestWeatherForecasts: (startDateIndex: number): AppThunkAction<KnownAction> => (dispatch, getState) => {
4646
// Only load data if it's something we don't already have (and are not already loading)
4747
if (startDateIndex !== getState().weatherForecasts.startDateIndex) {
4848
let fetchTask = fetch(`/api/SampleData/WeatherForecasts?startDateIndex=${ startDateIndex }`)
4949
.then(response => response.json())
5050
.then((data: WeatherForecast[]) => {
51-
dispatch(new ReceiveWeatherForecasts(startDateIndex, data));
51+
dispatch({ type: 'RECEIVE_WEATHER_FORECASTS', startDateIndex: startDateIndex, forecasts: data });
5252
});
5353

5454
addTask(fetchTask); // Ensure server-side prerendering waits for this to complete
55-
dispatch(new RequestWeatherForecasts(startDateIndex));
55+
dispatch({ type: 'REQUEST_WEATHER_FORECASTS', startDateIndex: startDateIndex });
5656
}
5757
}
5858
};
5959

6060
// ----------------
6161
// REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state.
62+
6263
const unloadedState: WeatherForecastsState = { startDateIndex: null, forecasts: [], isLoading: false };
63-
export const reducer: Reducer<WeatherForecastsState> = (state, action) => {
64-
if (isActionType(action, RequestWeatherForecasts)) {
65-
return { startDateIndex: action.startDateIndex, isLoading: true, forecasts: state.forecasts };
66-
} else if (isActionType(action, ReceiveWeatherForecasts)) {
67-
// Only accept the incoming data if it matches the most recent request. This ensures we correctly
68-
// handle out-of-order responses.
69-
if (action.startDateIndex === state.startDateIndex) {
70-
return { startDateIndex: action.startDateIndex, forecasts: action.forecasts, isLoading: false };
71-
}
64+
65+
export const reducer: Reducer<WeatherForecastsState> = (state: WeatherForecastsState, action: KnownAction) => {
66+
switch (action.type) {
67+
case 'REQUEST_WEATHER_FORECASTS':
68+
return {
69+
startDateIndex: action.startDateIndex,
70+
forecasts: state.forecasts,
71+
isLoading: true
72+
};
73+
case 'RECEIVE_WEATHER_FORECASTS':
74+
// Only accept the incoming data if it matches the most recent request. This ensures we correctly
75+
// handle out-of-order responses.
76+
if (action.startDateIndex === state.startDateIndex) {
77+
return {
78+
startDateIndex: action.startDateIndex,
79+
forecasts: action.forecasts,
80+
isLoading: false
81+
};
82+
}
83+
break;
84+
default:
85+
// The following line guarantees that every action in the KnownAction union has been covered by a case above
86+
const exhaustiveCheck: never = action;
7287
}
73-
74-
// For unrecognized actions (or in cases where actions have no effect), must return the existing state
75-
// (or default initial state if none was supplied)
88+
7689
return state || unloadedState;
7790
};

templates/ReactReduxSpa/ClientApp/store/index.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { ActionCreatorGeneric } from 'redux-typed';
21
import * as WeatherForecasts from './WeatherForecasts';
32
import * as Counter from './Counter';
43

@@ -18,4 +17,6 @@ export const reducers = {
1817

1918
// This type can be used as a hint on action creators so that its 'dispatch' and 'getState' params are
2019
// correctly typed to match your store.
21-
export type ActionCreator = ActionCreatorGeneric<ApplicationState>;
20+
export interface AppThunkAction<TAction> {
21+
(dispatch: (action: TAction) => void, getState: () => ApplicationState): void;
22+
}

templates/ReactReduxSpa/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
"react-router-redux": "^4.0.6",
3636
"redux": "^3.6.0",
3737
"redux-thunk": "^2.1.0",
38-
"redux-typed": "^2.0.0",
3938
"style-loader": "^0.13.0",
4039
"ts-loader": "^0.8.1",
4140
"typescript": "2.0.3",

templates/ReactReduxSpa/webpack.config.vendor.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module.exports = {
1515
]
1616
},
1717
entry: {
18-
vendor: ['bootstrap', 'bootstrap/dist/css/bootstrap.css', 'domain-task', 'event-source-polyfill', 'react', 'react-dom', 'react-router', 'redux', 'redux-thunk', 'react-router-redux', 'redux-typed', 'style-loader', 'jquery'],
18+
vendor: ['bootstrap', 'bootstrap/dist/css/bootstrap.css', 'domain-task', 'event-source-polyfill', 'react', 'react-dom', 'react-router', 'redux', 'redux-thunk', 'react-router-redux', 'style-loader', 'jquery'],
1919
},
2020
output: {
2121
path: path.join(__dirname, 'wwwroot', 'dist'),

0 commit comments

Comments
 (0)