-
Run
yarn create react-app my-app --typescript cd my-app
-
Install dev dependencies:
yarn add --dev husky lint-staged node-sass npm-run-all prettier stylelint stylelint-config-recommended tslint tslint-react tslint-config-prettier
-
Fix up the
tsconfig.json
, by enabling the following:"allowJs": false, // We don't need it "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, "target": "es6", // Babel means we don't need helpers
-
Create a
tslint.json
, with contents:{ "extends": ["tslint:latest", "tslint-react", "tslint-config-prettier"], "rules": { "interface-name": [true, "never-prefix"], "no-bitwise": false, "no-console": [true, "log"], "no-shadowed-variable": [ true, { "function": false, "temporalDeadZone": false } ], "no-this-assignment": [true, { "allow-destructuring": true }], "object-literal-sort-keys": false } }
-
Add
.stylelintrc.json
with contents{ "extends": "stylelint-config-recommended" }
-
Add
.prettierrc.json
with contents{ "tabWidth": 4, "trailingComma": "all" }
-
Fix up the
package.json
. Add some useful scripts:"format-file": "prettier --write", "format": "find . | grep -v node_modules | grep -v dist | egrep '\\.(js(on)?|md|scss|tsx?)?$' | xargs yarn run format-file", "lint-style": "stylelint 'src/**/*.scss'", "lint-ts-file": "tslint", "lint-ts": "tslint --project .", "lint": "npm-run-all lint-style lint-ts", "precommit": "lint-staged", "typecheck-watch": "tsc --noEmit --watch", "typecheck": "tsc --noEmit",
and then a
husky
block andlint-staged
block:"husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "**/*.{js,json,md,scss}": [ "yarn run format-file", "git add" ], "**/*.{ts,tsx}": [ "yarn run lint-ts-file --fix", "yarn run format-file", "git add" ] },
After all this, these two blocks look like this:
"scripts": { "build": "react-scripts build", "eject": "react-scripts eject", "format-file": "prettier --write", "format": "find . | grep -v node_modules | grep -v dist | egrep '\\.(js(on)?|md|scss|tsx?)?$' | xargs yarn run format-file", "lint-style": "stylelint 'src/**/*.scss'", "lint-ts-file": "tslint", "lint-ts": "tslint --project .", "lint": "npm-run-all lint-style lint-ts", "start": "react-scripts start", "test": "react-scripts test", "typecheck-watch": "tsc --noEmit --watch", "typecheck": "tsc --noEmit" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "**/*.{js,json,md,scss}": [ "yarn run format-file", "git add" ], "**/*.{ts,tsx}": [ "yarn run lint-ts-file --fix", "yarn run format-file", "git add" ] },
-
Go ahead and run
yarn format
now, to get everything formatted the way we like it. -
Rejoice that the environment is set up, and celebrate with a commit.
Install common libraries we'll be using. Every project probably wants these.
yarn add classnames react-redux redoodle redux reselect
yarn add --dev @types/classnames, @types/react-redux
Some other libraries that I like, but aren't needed for every app:
For routing:
(@types/)history
(@types/)redux-first-router
(@types/)redux-first-router-link
(@types/)query-string
For sagas:
(@types/)redux-saga
For general data manipulation:
transducist
Make some files and directories in src/
:
components/
App.tsx
redux/
mySubstate/
mySubstateActions.ts
mySubstateReducer.ts
mySubstateSaga.ts
createStore.ts
reducers.ts
rootSaga.ts
index.tsx
interfaces.ts
Most of the steps are the same, but to get started
create-react-native-app my-app --scripts-version=react-native-scripts-ts
We don't need to do anything involving SASS or Stylelint. This means the added scripts can look like
"format": "find . | grep -v node_modules | grep -v dist | egrep '\\.(js(on)?|md|tsx?)?$' | xargs yarn run format-file",
"lint-file": "tslint",
"lint": "tslint --project .",
"precommit": "lint-staged",
"typecheck-watch": "tsc --noEmit --watch",
"typecheck": "tsc --noEmit",
and the "lint-staged"
block
"lint-staged": {
"**/*.{js,json}": [
"yarn run format-file",
"git add"
],
"**/*.{ts,tsx}": [
"yarn run lint-file --fix",
"yarn run format-file",
"git add"
]
},
Other than that, all steps are equivalent.
import createBrowserHistory from "history/createBrowserHistory";
import queryString from "query-string";
import {
Action,
combineReducers,
CompoundAction,
createStore,
loggingMiddleware,
reduceCompoundActions,
} from "redoodle";
import { applyMiddleware, compose } from "redux";
import { connectRoutes, LocationState } from "redux-first-router";
import createSagaMiddleware from "redux-saga";
import { RootState, Store } from "../interfaces";
import { reducers } from "./reducers";
import { rootSaga } from "./rootSaga";
import { routesMap } from "./routes";
export function configureStore(): Store {
const history = createBrowserHistory();
const routing = connectRoutes(history, routesMap, {
initialDispatch: false,
querySerializer: queryString,
});
const sagaMiddleware = createSagaMiddleware({
// Make sure sagas flatten Redoodle's compound actions.
emitter: emit => action => {
const handleAction = (a: Action) => {
if (CompoundAction.is(a)) {
a.payload.forEach(handleAction);
} else {
emit(a);
}
};
handleAction(action);
},
});
const middlewares = applyMiddleware(
routing.middleware,
sagaMiddleware,
loggingMiddleware(),
);
const rootReducer = combineReducers({
...reducers,
location: routing.reducer,
});
const initialLocationState = routing.reducer(undefined, {
type: "__unknown__",
});
const store = createStore(
reduceCompoundActions(rootReducer),
getInitialState(initialLocationState),
// Redoodle and Redux have incompatible types for Reducer, so we need to
// ram the types through in this one place. See
// https://github.com/palantir/redoodle/issues/4.
compose(
routing.enhancer,
middlewares,
) as any,
);
sagaMiddleware.run(rootSaga);
routing.initialDispatch!();
return store;
}
function getInitialState(location: LocationState): RootState {
return { blah: {}, location };
}
export function* rootSaga(): SagaIterator {
// Spawn rather than fork so that one saga's failure does not doom the
// others.
let tasks: Task[] = [];
try {
tasks = [yield spawn(wordSaga), yield spawn(routesSaga)];
} finally {
if (yield cancelled()) {
yield all(tasks.map(task => cancel(task)));
}
}
}
import { TypedAction } from "redoodle";
import {
Action as RouteAction,
NOT_FOUND,
RoutesMap,
} from "redux-first-router";
import { SagaIterator } from "redux-saga";
import { put, takeLatest } from "redux-saga/effects";
export interface IndexRoutePayload {
word: string;
}
export const indexRoute = TypedAction.define("INDEX_ROUTE")<
IndexRoutePayload
>();
export const routesMap: RoutesMap = { [indexRoute.TYPE]: "/:word" };
export function* routesSaga(): SagaIterator {
yield takeLatest([...Object.keys(routesMap), NOT_FOUND], handleRouteChange);
}
function* handleRouteChange(action: RouteAction): SagaIterator {
if (action.type === NOT_FOUND) {
yield put(indexRoute({ word: "wordless" }));
}
}