This project was created to demonstrate how to set up and organize a project using:
It also demonstrates how to set up Jest for unit testing, and TSLint for linting.
Inspiration and instruction for this project was taken from the following blog posts and documention.
- https://scotch.io/tutorials/setup-a-react-environment-using-webpack-and-babel
- https://www.typescriptlang.org/docs/handbook/react-&-webpack.html
- Some parts out of date: microsoft/TypeScript#13873
- https://webpack.github.io/docs/webpack-dev-server.html
- https://github.com/facebook/jest/tree/master/examples/typescript
- https://spin.atomicobject.com/2016/09/27/typed-redux-reducers-typescript-2-0/
This project uses the following organizational structure
.
|-- index.html
|-- client/
|-- action-creators/
|-- action-types/
|-- components/
|-- containers/
|-- reducers/
|-- application-state.ts
|-- index.tsx
|-- dist/
|-- bundle.js
|-- webpack.config.js
|-- tsconfig.json
|-- tslint.json
|-- package.json
|-- node_modules/
where the above directories and files correspond to the following:
index.html
- Html page served up to the clientclient/
- Source codeclient/action-creators/
- Redux actions and creatorsclient/action-types/
- Redux actions typesclient/components/
- React componentsclient/containers/
- Redux containers for componentsclient/reducers/
- Redux reducersclient/application-state.ts
- Redux application stateclient/index.tsx
- Entry point for the javascript code
dist/
- Output directory for transpiled codedist/bundle.js
- Transpiled application
webpack.config.js
- Webpack configuration filetsconfig.json
- TypeScript configuration filetslint.json
- TSLint configurationpackage.json
- Project configuration filenode_modules/
- Where dependencies are installed to
You can choose to manage dependencies using either yarn or npm. As of early 2017 it's not clear if yarn will become the defacto standard, but it seems to be gaining popularity. These instructions will use yarn
, but you can also use npm
with minimal tweaks to the following instructions.
You can find instructions for installing yarn here
Create a new directory, cd
into it and initialize your project via:
yarn init
This will take ask your a series of questions, and will generate a package.json
file based on how you answer them. You can always update the package.json
file in the future, so don't feel like you have to configure everything correctly out of the box.
This section describes how to install all of the required project dependencies using yarn.
For every yarn/npm library, there are usually types defined for it in the DefinitelyTyped project. Those type can be added by installing @types/[normal library name]
, where [normal library name]
is the name of the library.
We will use webpack to manage the compilation of our TypeScript code. Install webpack, and webpack-dev-server by running:
yarn add webpack webpack-dev-server
Install React with type definitions by running:
yarn add react react-dom @types/react @types/react-dom
Install Redux for usage with react with type definitions by running:
yarn add redux react-redux @types/redux @types/react-redux
Install TypeScript by running:
yarn add typescript awesome-typescript-loader --dev
This project uses awesome-typescript-loader
for TypeScript compilation. The TypeScript docs recommend using it. However, ts-loader
is also mentioned as an alternative. I have not used it, but it may be worth investigating.
This project uses Jest as its test runner. Install it and some supporting libraries by running:
yarn add jest ts-jest react-addons-test-utils --dev
After installing all of the above dependencies, you sould have a node_modules
directory, yarn.lock
file, and a package.json
file that includes all of the dependencies. The package.json
file should look like this:
{
"name": "Your Project Name",
"version": "1.0.0",
"description": "Your Description",
"main": "index.tsx",
"author": "Your Name",
"license": "Your License",
"scripts": {
...
},
"dependencies": {
...
},
"devDependencies": {
...
}
}
The dependencies
and devDependencies
sections should be populated by the libraries we just installed.
The next step is to add configuration files for Webpack, TypeScript, and Jest.
Create a webpack.config.js
file, and update it to look something like this.
const path = require('path');
module.exports = {
entry: './client/index.tsx',
output: {
path: path.resolve('dist'),
publicPath: "/dist/",
filename: 'bundle.js'
},
devtool: "source-map",
resolve: {
// Add '.ts' and '.tsx' as resolvable extensions.
extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"]
},
module: {
loaders: [
{ test: /\.tsx$/, loader: 'awesome-typescript-loader' },
],
}
}
The webpack.config.js
file defines the entry point for our javascript code to live in ./client/index.tsx
, and specifies that the compiled javascript be placed in ./dist/bundle.js
. The loaders
section describes how to process different file types. We are informing webpack to use the awesome-typescript-loader
when processing .ts
and .tsx
files.
Create a TypeScript configuration file called tsconfig.json
with the following contents:
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"noUnusedLocals": true,
"lib": [
"es5",
"es6",
"dom"
]
},
"include": [
"./client/**/*"
]
}
You can reference the TypeScript docs to understand the different compilerOptions
and what they do. The above configuration should be enough to get off the ground.
Add the following to your package.json
file, per the ts-jest
instructions.
"jest": {
"transform": {
".(ts|tsx)": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js"
]
},
Install TSLint and run tslint --init
to generate a base tslint.json
file. I recommend using tslint-eslint-rules as a base rule set. I have written about TSLint here. The configuration used in this project looks like this:
{
"extends": [
"tslint:recommended",
"tslint-eslint-rules"
],
"jsRules": {},
"rules": {
"quotemark": [true, "single"],
"ter-indent": [true, 2],
"interface-name": [true, "never-prefix"],
"no-empty": false,
"import-sources-order": "any",
"ordered-imports": false
},
"rulesDirectory": []
}
We need to define the base HTML file that our application will live in. I recommend using something simple like the following:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sample React-Redux-TypeScript Project</title>
</head>
<body>
<div id="app"> </div>
<script src="./dist/bundle.js"> </script>
</body>
</html>
Place the above HTML in an index.html
file in the root of the project. This file includes a div
where we will load our application, and the compiled bundle.js
javascript file.
We can add the following script to our package.json
file to allow us to start a webpack-dev-server
pointing at the above HTML file. To do this, add the following to the scripts
section the package.json
file.
"scripts": {
"start": "webpack-dev-server --content-base ./"
}
This example app, like every other React/Redux example app contains a greeting message the that user can change, and a button that increments a counter:
The application state is modeled in the following way:
export interface ApplicationState {
greeting: string
count: number;
};
export const defaultState: ApplicationState = {
greeting: 'React-TypeScript-Redux Example',
count: 0
};
Notice how we can now define our state using types!
There are two main components - Greeting
and Increment
. Both are included in a wrapper component called App
:
class App extends React.Component<{}, {}> {
render() {
return (
<div style={{textAlign: 'center'}}>
<Greeting />
<Increment />
</div>
);
}
}
The React type definitions specify two generic types for React.Component<P, S>
. P
is the type of the props for the component, and S
is the type of the component state. If you are using Redux, you will most likely store all of your state in the redux store. If you do store any state local to a component, you can use S
do define the shape of that state.
To describe the component and container organization I will refer to an example - the Greeting
component and container.
The Greeting
component is defined as:
export interface Props {
greeting: string;
updateGreeting: (greeting: string) => void;
};
export default class Greeting extends React.Component<Props, {}> {
...
}
Here, the Greeting
component is completely unaware of the container that connects it to Redux. It simply renders itself using the defined Props
. This makes it super testable.
As with standard Redux, the Greeting
container connects the Greeting
component to the Redux store. It uses the exported component Props
and splits the props into two sets - State
props and Dispatch
props:
import { Props as GreetingProps } from '../components/greeting';
type StateProps = Pick<GreetingProps, 'greeting'>;
type DispatchProps = Pick<GreetingProps, 'updateGreeting'>;
Using those two sets of props, it defines the standard Redux mapStateToProps
, and mapDispatchToProps
functions.
function mapStateToProps(state: ApplicationState): StateProps {
return { greeting: state.greeting };
};
function mapDispatchToProps(dispatch: Dispatch<any>): DispatchProps {
return {
updateGreeting: (newGreeting: string) => {
dispatch(Actions.updateGreeting(newGreeting));
},
};
};
These functions are provided to the Redux connect
function and exported
const ConnectedGreeting = connect(
mapStateToProps,
mapDispatchToProps,
)<{}>(GreetingComponent);
export default ConnectedGreeting;
The container can then be used elsewhere and will be connected to the Redux store to make and receive application state updates. Note the empty generic type {}
between the connect call and the invocation to GreetingComponent
. This is the external props type of the container. So, if you wish to define any props when using the container, you will need to define the shape of those props there.
One area in particular that types can be handy in React/Redux applications is inside of Redux reducers. This article describes a really nice approach for writing strongly typed reducers.
Using TypeScript we can define our actions in a type safe manner.
export type UpdateGreetingAction = {
type: ActionTypes.UPDATE_GREETING,
greeting: string
}
export type IncrementAction = {
type: ActionTypes.INCREMENT
}
We can then create a union type in our reducer, combining all of our individual action types:
type Action = Actions.UpdateGreetingAction | Actions.IncrementAction;
Then, when we switch on the action type, we are guarenteed type safety when updating our state:
const updateState = (state: ApplicationState = defaultState, action: Action) => {
switch(action.type) {
case ActionTypes.UPDATE_GREETING:
return {
greeting: action.greeting,
count: state.count
}
case ActionTypes.INCREMENT:
return {
greeting: state.greeting,
count: state.count + 1
}
default:
return state;
}
};
If we were to update the ActionTypes.INCREMENT
case to set greeting
to action.greeting
instead of state.greeting
, we would receive a compiler error stating that:
ERROR in [at-loader] client/reducers/index.ts:16:24
TS2339: Property 'greeting' does not exist on type 'IncrementAction'.
To build the TypeScript code and produce an output bundle.js
file run
webpack
To start webpack-dev-server
running run
yarn start
To run the jest
tests run
yarn test
To lint the project run
yarn lint
The Redux dev tools extension is a nice way to visualize the redux state changes in your app, and debug issues that might arise. There are two steps required to install it:
- Install the browser extension - this varies based on your browser, but there are instructions in the above link.
- Update how you instantiate your Redux store - The second step requires you to insert a debug hook into your code when you instantiate your Redux store. There are several ways to do this depending on your application. The easiest way (explained in the above link) involves adding an extra parameter when you call
createStore
, and looks like this:
const store = createStore(
updateState,
(window as any).__REDUX_DEVTOOLS_EXTENSION__ && (window as any).__REDUX_DEVTOOLS_EXTENSION__()
);
If you also need to include other middleware when you create your store (e.g., for something like redux-thunk
you can use the following:
import thunk from 'redux-thunk';
import { createStore, applyMiddleware } from 'redux';
const enhancer = (window as any).__REDUX_DEVTOOLS_EXTENSION__ ? (window as any).__REDUX_DEVTOOLS_EXTENSION__()(createStore) : createStore;
const store = enhancer(updateState, applyMiddleware(thunk));
I found this solution in this issue on the Redux dev tools extension project.