Skip to content

Instantly share code, notes, and snippets.

@dphilipson
Last active January 11, 2024 16:12
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dphilipson/2a6377b5aa5e6bc70588735f6175e152 to your computer and use it in GitHub Desktop.
Save dphilipson/2a6377b5aa5e6bc70588735f6175e152 to your computer and use it in GitHub Desktop.
React/TypeScript setup notes

React/TypeScript setup steps

Setting up the environment

  • 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 and lint-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.

Getting the project rolling

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

Differences in React Native

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.

Recipes

Creating a store with Redux-First-Router, Redux-Saga, and Redoodle

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 };
}

Root saga

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)));
        }
    }
}

Route configuration

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" }));
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment