Skip to content
This repository has been archived by the owner on Dec 10, 2023. It is now read-only.

An easy and fast way to start writing kickass React Redux Typescript web apps

License

Notifications You must be signed in to change notification settings

sharpcoding/react-redux-typescript-starter-kit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

81 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

React Redux TypeScript Starter Kit

Quickstart

git clone https://github.com/sharpcoding/react-redux-typescript-starter-kit.git
cd react-redux-typescript-starter-kit
npm i
npm run dev

everything goes OK, a new browser tab window tab opens automatically pointing to http://localhost:9000/.

Chrome Web Browser with Redux DevTools Extension is recommended. You can play around with user interface and see actions with store changes in Redux DevTools Inspector, change code and see updates as they appear (by hot-reloading).

alt text

If this seems too much, see the running demo.

Purpose

This starter kit summarizes good practices in front-end development, focused around the following front-end technology stack:

The purpose of this repository is to:

  • encourage developers community to use TypeScript with React Redux development,
  • provide a fast way to start writing mature and streamlined front-end applications.

General TypeScript remarks

TypeScript is - we believe - a great way to write single page applications today. Besides type-safety (understood as detecting errors directly in IDE or at transpilation phase), there plenty of other benefits:

  • targeting different existing/old ECMAScript versions
  • targeting future ECMAScript versions, i.e. incorporating recommendations/proposals - right here and right now
  • seamless handling of different bundle systems like UMD, AMD, CommonJS
  • intellisense in the IDE (!)
  • dedicated linter (tslint)
  • active development and great community

TypeScript is a (kind of) powered exoskeleton over ECMAScript (of the development phase)

Having this said, it should be emphasized that TypeScript is not used in runtime, so we don't talk about a kinda ECMAScript-on-steroids replacement for your browser - TypeScript is created for developers and for development purposes only.

Despite providing lots of benefits, there are special ares of interest when developing with TypeScript:

  • at first, doing things right in TypeScript might be challenging for a seasoned JavaScript developer; as every technology it requires a little bit to learn as there are something like 15+ major versions published; the good news is: it is not "all or nothing" domain, adopting to TypeScript might (and should) be gradual/incremental
  • despite popular belief, not every JavaScript statement is a correct TypeScript statement, this gets clear especially when dealing with JavaScript types implicit conversions (yet implicit conversions is the last thing we do want to happen when using TypeScript)
  • to summarize two points above: using TypeScript might require a little bit of investment and, more importantly, changed mindset
  • partial import in TypeScript is as good as the typings (d.ts) are, e.g. lodash typings do not currently support partial loading, resulting in much bigger bundle sizes
  • loading module variables out of the SCSS into TypeScript module is not seamless - it requires writing a dedicated scss.d.ts file
  • some libraries that might quite well in pure ECMAScript, are getting cumbersome when used in TypeScript (for real type safety and other benefits, it is not enough to provide typings to a JavaScript library / module)

Decisions made

  • Feature-oriented (not role-oriented) repository code structure
    • Typescript and Webpack path aliases (defining "semantic namespaces")
      • @components defines a path alias namespace for reusable / universal presentational components; to emphasize: these components are not containers and know nothing about Redux state
      • @views (optional) - for complex, reusable presentational components
      • @screens; a screen is 1) a graphical interface user sees after navigating with an url 2) an aggregate of components gathered together for a well defined purpose
      • @store defines a path alias namespace for everything related to the Redux Store with one remark: screens and components define and re-export interfaces to be assembled and reused by the main store interface (IAppState in our case)
    • Every container component - especially a screen - should define the following Redux-related modules:
      • action-creators.ts
      • action-types.ts
      • actions.ts
      • action-creators.ts
      • reducers.ts
      • model.ts defining an interface OR
      • models folder defining interfaces (one per file/module preferred); if a screen container component props are identical with interface used in the Redux store (e.g. EngineScreenState), it should be definedin model/state.ts, re-exported in index.tsx (read-on) and used in store composition (IAppState interface)
    • complex screen containers should be composed of multiple view container/presentational components
    • presentational components vs container components naming recommendation:
      • if a component is a container (bound to Redux store), then its' name should contain the Container suffix, e.g. MainScreenContainer
      • if a component is the presentational (i.e. pure React component), then it should be named without Container suffix e.g. just MainScreen
    • view is a container/presentational component that exists to make better screen decomposition; it should aggregate at least two other presentational components; if it defines it's own Redux-related modules (action-creators.ts, ..., state.ts), then it is a container
    • having the whole React application visualized as a tree of components, avoid creating container components in the leaf nodes; it might happen a component is found reusable and replaced to @components path alias namespace
  • index.ts/index.tsx re-exports
  • Redux store:
    • React state is a TypeScript interface
    • Action types are defined as TypeScript constants
    • Actions are defined as TypeScript classes
    • Actions acceptable by a reducer make-up a TypeScript discriminated union type
    • Action creators are plain functions
    • Reducers are plain functions
    • Redux-thunk effects are higher-order functions
  • tslint with VSCode tslint extension
    • indentation with 2 spaces
  • Webpack-npm scripts for development, publishing and bundle analysis
  • Bootstrap v4
  • Custom SCSS with variables exported to ECMAScript
  • Jest snapshot testing

Throughout paragraphs below I go a little bit into decision details regarding TypeScript Redux programming, yet, it all can be summarized with the following rule:

use constructs and solutions provided by TypeScript whenever possible

This makes application of helper-libraries like react-actions redundant. Referring to react-actions typings example, I found it really cumbersome, awkward to use and - in the context of type safety - limiting.

Another example might be react-bootstrap - as for version 0.3 it provides little or no benefits compared to using plain Bootstrap (e.g. no forms validation, still targeting Bootstrap v3), yet it makes another dependency in package.json.

Redux

Solutions applied in the Redux section are influenced by Reactive libraries for Angular

State is just an interface

interface IEngine {
  started: boolean;
  currentGear: number;
}

Action types are defined as constants (not plain strings)

Do not use / reuse Redux actions as plain texts:

export const START_ENGINE = 'START_ENGINE';
export const GEARS_UP_DOWN = 'GEARS_UP_DOWN';

Actions are defined as ES6 classes

Action is just an object. Rearding Redux store requirement it is a plain object having the type property and every action object meets the following contract:

interface Action {
  type: string;
}

Actions are defined by the class construct, which is a great way to:

  • keep action and it's type in the same place
  • define action properties by constructor arguments with public modifier.
import { Action } from 'redux';
import * as actionTypes from './action-types';

class StartEngineAction implements Action {
  public readonly type = actionTypes.START_ENGINE;
}

class GearsUpDownAction implements Action {
  public readonly type = actionTypes.GEARS_UP_DOWN;
  constructor(public gears: number) { }
}

Actually, the ES6 classes not only describe actions - when used with new keyword, these are in fact the action creators (please read on). For this reason - and because actions objects created this way are "not plain" - this aspect of TypeScript Redux approach might change in the future.

Action creators are plain functions

Action creators are functions that create actions. The responsibility of action creators is to:

  • create action objects,
  • make them "plain" (not prototype-linked to any function) again,
  • expose the some kind of API to Redux store application, as an example here we see the action creator disallowing user to change more than one gear at a time
import * as _ from 'lodash';
import { StartEngineAction, GearsUpAction } from './actions';

type IStartEngineActionCreator = () => StartEngineAction;
type IGearUpActionCreator = () => GearsUpDownAction;
type IGearDownActionCreator = () => GearsUpDownAction;

const start: IStartEngineActionCreator = () =>
  _.toPlainObject(new StartEngineAction());

const gearUp: IGearUpActionCreator = () =>
  _.toPlainObject(new GearsUpDownAction(1));

const gearDown: IGearDownActionCreator = () =>
  _.toPlainObject(new GearsUpDownAction(-1));

export {
  start,
  IStartEngineActionCreator,
  gearUp,
  IGearUpActionCreator,
  gearDown,
  IGearDownActionCreator,
};

Please note these simple functions are described by TypeScript type aliases (in order to reuse in React containers as prop types !)

Reducers are plain functions

Reducers are plain functions that make use of discriminated union types - this is big win for writing reducer's code, having an appropriate action type (one of an union) "magically" cast in the relevant case block. Additionally, any breaking changes to:

  • action types defined,
  • action definitions that are available,
  • available action parameters and types will be easily detected by TypeScript and reported as an error.
import * as _ from 'lodash';
import * as engineActionTypes from './action-types';
import { StartEngineAction, GearsUpDownAction } from './actions';
import { IEngine } from './state';

const initialState: IEngine = { started: false, currentGear: 1 };

export type EngineReducerActionTypes = StartEngineAction|GearsUpDownAction;

export const engineReducer = (state: IEngine = initialState, action: EngineReducerActionTypes): IEngine => {
  switch (action.type) {
    case engineActionTypes.START_ENGINE:
      return { ...state, started: true } as IEngine;
    case engineActionTypes.GEARS_UP_DOWN:
      return { ...state, currentGear: state.currentGear + action.gears } as IEngine;
    default:
      return state;
  }
};

Effects for complex and asynchronous scenarios

We believe there is nothing like an "async action" as well as "async action-creator". Certainly, in the scenarios which are

  • simple
  • do not involve some kind of asynchrony

plain actions and action creators are sufficient.

However, in more complex (most often asynchronous) cases, when a single activity in user interface results in casting several actions, developers are highly encouraged to consciously use the "effect" terminology.

For asynchronous scenarios, effect is just an higher-order function (a function that returns other function, in the case of redux-thunk, the one with dispatch argument).

As an example, lets imagine a two gears up change in the engine is asynchronous and we want to make it in a safe way:

import * as _ from 'lodash';
import { Dispatch } from 'react-redux';
import { 
  PressingClutchAction, 
  GearsUpDownAction,
  ReleasingClutchAction
} from './actions';

type ITwoGearsUpEffect = () => (dipatch: Dispatch<void>) => void;

const twoGearsUp: ITwoGearsUpEffect = () => (dispatch: Dispatch<void>) => {
  dispatch(_.toPlainObject(new PressingClutchAction()))
  new Promise((resolve, reject) => setTimeout(() => resolve(), 1500))
  .then(() => { 
    dispatch(_.toPlainObject(new GearsUpDownAction(2)))
    dispatch(_.toPlainObject(new ReleasingClutchAction()))
  });
};

export {
  twoGearsUp,
  ITwoGearsUpEffect,
};

Webpack and Typescript path aliases

import { BubbleChart } from '@components/bubble-chart';

instead of:

import { BubbleChart } from '../../../src/components';

About

An easy and fast way to start writing kickass React Redux Typescript web apps

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published