├── .codeclimate.yml ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── codecov.yml ├── declarations ├── declarations.d.ts ├── jest-glamor-react.d.ts └── react-test-renderer.d.ts ├── hotReloadingServer.js ├── index.html ├── package.json ├── postcss.config.js ├── server.js ├── src ├── __mocks__ │ └── fileMock.js ├── __tests__ │ ├── actions │ │ ├── actionCreators.test.ts │ │ └── actionTypes.test.ts │ ├── components │ │ ├── MarkdownText.test.tsx │ │ ├── TextArea.test.tsx │ │ └── __snapshots__ │ │ │ ├── MarkdownText.test.tsx.snap │ │ │ └── TextArea.test.tsx.snap │ ├── containers │ │ ├── App.test.tsx │ │ └── __snapshots__ │ │ │ └── App.test.tsx.snap │ ├── reducers │ │ └── applicationReducer.test.ts │ ├── redux │ │ ├── observable │ │ │ ├── textChange.test.ts │ │ │ └── utils.ts │ │ └── saga │ │ │ └── textChange.test.ts │ └── store │ │ ├── reduxObservableStore.test.ts │ │ └── reduxSagaStore.test.ts ├── actions │ ├── actionCreators.ts │ └── actionTypes.ts ├── components │ ├── Card.tsx │ ├── MarkdownText.tsx │ └── TextArea.tsx ├── containers │ └── app.tsx ├── index.tsx ├── models │ └── applicationState.ts ├── reducers │ └── applicationReducer.ts ├── redux │ ├── observable │ │ ├── ScheduledEpic.ts │ │ ├── root.ts │ │ └── textChange.ts │ └── saga │ │ ├── root.ts │ │ └── textChange.ts └── store │ ├── reduxObservableStore.ts │ └── reduxSagaStore.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - javascript 8 | - typescript 9 | ratings: 10 | paths: 11 | - "**.js" 12 | - "**.jsx" 13 | - "**.ts" 14 | - "**.tsx" 15 | exclude_paths: [] 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "rules": {} 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.awcache 2 | /.vscode 3 | /node_modules 4 | /dist 5 | /coverage 6 | /.idea 7 | .DS_Store 8 | yarn-error.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | after_success: 5 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Kalle Ott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build status](https://travis-ci.org/kaoDev/react-ts-sample.svg?branch=master)](https://travis-ci.org/kaoDev/react-ts-sample) 2 | [![Code Climate](https://codeclimate.com/github/kaoDev/react-ts-sample/badges/gpa.svg)](https://codeclimate.com/github/kaoDev/react-ts-sample) 3 | [![codecov.io](https://codecov.io/github/kaoDev/react-ts-sample/coverage.svg?branch=master)](https://codecov.io/gh/kaoDev/react-ts-sample?branch=master) 4 | ![license](https://img.shields.io/github/license/kaoDev/react-ts-sample.svg) 5 | 6 | ### Sample techstack for [React](https://facebook.github.io/react/), [redux](redux.js.org), [redux-saga](https://github.com/yelouafi/redux-saga)/[redux-observable](https://redux-observable.js.org/), [TypeScript](https://github.com/Microsoft/TypeScript), [webpack](https://github.com/webpack/webpack) 7 | 8 | As a usecase here is an simple markdown writer implementation, 9 | to bring in an async scenario the user input is debounced. 10 | 11 | For async actions there is an implementation with redux-saga and redux-observable, 12 | they exist side by side and can be activated in the code. 13 | To Choose the wanted implementation change the import of 14 | ```createApplicationStore``` to the wanted technology. At the moment there are 2 15 | stores available: 16 | 17 | 1. [```reduxObservableStore.ts```](src/store/reduxObservableStore.ts) 18 | 2. [```reduxSagaStore.ts```](src/store/reduxSagaStore.ts) 19 | 20 | 21 | startup sample: 22 | 23 | ``` 24 | yarn install 25 | yarn start 26 | ``` 27 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: true 4 | comment: 5 | behavior: default 6 | layout: header, diff 7 | require_changes: false 8 | coverage: 9 | precision: 2 10 | range: 11 | - 70.0 12 | - 100.0 13 | round: down 14 | status: 15 | changes: false 16 | patch: true 17 | project: true 18 | parsers: 19 | gcov: 20 | branch_detection: 21 | conditional: true 22 | loop: true 23 | macro: false 24 | method: false 25 | javascript: 26 | enable_partials: false 27 | -------------------------------------------------------------------------------- /declarations/declarations.d.ts: -------------------------------------------------------------------------------- 1 | // declare var module: any 2 | declare module 'react-hot-loader' 3 | 4 | // declare var require: { 5 | // (path: string): any 6 | // (path: string): T 7 | // (paths: string[], callback: (...modules: any[]) => void): void 8 | // ensure: (paths: string[], callback: (require: (path: string) => T) => void) => void 9 | // } 10 | 11 | interface Window { 12 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any 13 | } 14 | 15 | interface NodeModule { 16 | hot: any; 17 | } -------------------------------------------------------------------------------- /declarations/jest-glamor-react.d.ts: -------------------------------------------------------------------------------- 1 | declare const glamorSerializer: jest.SnapshotSerializerPlugin 2 | 3 | export default glamorSerializer 4 | -------------------------------------------------------------------------------- /declarations/react-test-renderer.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for react-test-renderer 15.5 2 | // Project: https://facebook.github.io/react/ 3 | // Definitions by: Arvitaly , Lochbrunner , Lochbrunner , John Reilly 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | // TypeScript Version: 2.3 6 | 7 | import { ReactElement, ReactInstance } from 'react' 8 | 9 | type TestRendererOptions = { 10 | createNodeMock: (element: ReactElement) => any 11 | } 12 | 13 | type ReactTestRendererJSON = { 14 | type: string 15 | props: { [propName: string]: any } 16 | children: null | Array 17 | $$typeof?: Symbol // Optional because we add it with defineProperty(). 18 | } 19 | type ReactTestRendererNode = ReactTestRendererJSON | string 20 | 21 | type Container = { 22 | children: Array 23 | createNodeMock: Function 24 | tag: 'CONTAINER' 25 | } 26 | 27 | type Props = Object 28 | type Instance = { 29 | type: string 30 | props: Object 31 | children: Array 32 | rootContainerInstance: Container 33 | tag: 'INSTANCE' 34 | } 35 | 36 | type TextInstance = { 37 | text: string 38 | tag: 'TEXT' 39 | } 40 | 41 | type FindOptions = { 42 | // performs a "greedy" search: if a matching node is found, will continue 43 | // to search within the matching node's children. (default: true) 44 | deep: boolean 45 | } 46 | 47 | export type Predicate = (node: ReactTestInstance) => boolean | undefined 48 | 49 | export interface ReactTestInstance

{ 50 | // Custom search functions 51 | find(predicate: Predicate): React.ReactElement 52 | 53 | findByType

(type: React.ComponentType

): React.ReactElement

54 | 55 | findByProps(props: Object): ReactTestInstance 56 | 57 | findAll( 58 | predicate: Predicate, 59 | options?: FindOptions 60 | ): Array> 61 | 62 | findAllByType

( 63 | type: React.ComponentType

, 64 | options?: FindOptions 65 | ): Array> 66 | 67 | findAllByProps( 68 | props: Object, 69 | options?: FindOptions 70 | ): Array> 71 | } 72 | 73 | export type RenderedComponent

= { 74 | root: ReactTestInstance

75 | 76 | toJSON(): ReactTestRendererJSON 77 | unmount(): void 78 | update(nextElement: ReactElement

): void 79 | getInstance(): ReactInstance 80 | } 81 | 82 | // https://github.com/facebook/react/blob/master/src/renderers/testing/ReactTestMount.js#L155 83 | export function create( 84 | nextElement: ReactElement, 85 | options?: TestRendererOptions 86 | ): RenderedComponent 87 | -------------------------------------------------------------------------------- /hotReloadingServer.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const WebpackDevServer = require('webpack-dev-server'); 3 | const config = require('./webpack.config'); 4 | 5 | const app = new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true 9 | }); 10 | 11 | const address = process.env.IP || "localhost"; 12 | const port = process.env.PORT || 3000; 13 | 14 | app.listen(port, address, (err, result) => { 15 | if (err) { 16 | return console.log(err); 17 | } 18 | console.log(`Chat server listening at ${address}:${port}`); 19 | }); 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React - Typescript - Redux - Glamorous 7 | 8 | 9 | 10 | 11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kiel-react-ts", 3 | "version": "0.1.0", 4 | "description": "Small sample to show usage of react with typescript", 5 | "scripts": { 6 | "start": "node hotReloadingServer.js", 7 | "build": "webpack", 8 | "test": "jest --coverage --no-cache" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/KalleOtt/react-ts-sample.git" 13 | }, 14 | "jest": { 15 | "transform": { 16 | ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" 17 | }, 18 | "mapCoverage": true, 19 | "testRegex": "src/__tests__/.*\\.test\\.(ts|tsx|js|jsx)$", 20 | "moduleFileExtensions": [ 21 | "ts", 22 | "tsx", 23 | "js", 24 | "json" 25 | ], 26 | "moduleNameMapper": { 27 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "src/__mocks__/fileMock.js", 28 | "\\.(s?css|less)$": "identity-obj-proxy" 29 | }, 30 | "snapshotSerializers": [ 31 | "jest-glamor-react" 32 | ], 33 | "collectCoverageFrom": [ 34 | "src/actions/*.{ts,tsx}", 35 | "src/components/*.{ts,tsx}", 36 | "src/containers/*.{ts,tsx}", 37 | "src/models/*.{ts,tsx}", 38 | "src/reducers/*.{ts,tsx}", 39 | "src/redux/*.{ts,tsx}", 40 | "src/store/*.{ts,tsx}" 41 | ], 42 | "moduleDirectories": [ 43 | "node_modules", 44 | "src" 45 | ] 46 | }, 47 | "author": "", 48 | "license": "MIT", 49 | "devDependencies": { 50 | "@types/jest": "^20.0.6", 51 | "@types/marked": "^0.3.0", 52 | "@types/react": "^16.0.1", 53 | "@types/react-dom": "^15.5.2", 54 | "@types/react-hot-loader": "^3.0.3", 55 | "@types/react-redux": "^4.4.47", 56 | "@types/redux-actions": "^1.2.7", 57 | "@types/redux-devtools": "^3.0.38", 58 | "@types/redux-mock-store": "^0.0.9", 59 | "@types/webpack": "^3.0.6", 60 | "awesome-typescript-loader": "^3.2.2", 61 | "express": "^4.15.4", 62 | "glamor": "^2.20.40", 63 | "glamorous": "^4.2.1", 64 | "identity-obj-proxy": "^3.0.0", 65 | "immutability-helper": "^2.3.0", 66 | "jest": "^20.0.4", 67 | "jest-glamor-react": "^3.1.0", 68 | "marked": "^0.3.6", 69 | "react": "^16.0.0-beta.5", 70 | "react-dom": "^16.0.0-beta.5", 71 | "react-hot-loader": "^3.0.0-beta.7", 72 | "react-redux": "^5.0.6", 73 | "react-test-renderer": "^16.0.0-beta.5", 74 | "redux": "^3.7.2", 75 | "redux-actions": "^2.2.1", 76 | "redux-devtools": "^3.4.0", 77 | "redux-mock-store": "^1.2.3", 78 | "redux-observable": "^0.14.1", 79 | "redux-saga": "^0.15.6", 80 | "rxjs": "^5.4.2", 81 | "source-map-loader": "^0.2.1", 82 | "ts-jest": "^20.0.10", 83 | "typescript": "^2.4.2", 84 | "webpack": "^3.5.1", 85 | "webpack-dev-server": "^2.7.1" 86 | }, 87 | "dependencies": { 88 | "rgba-string": "^1.0.7" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | 'postcss-cssnext': { 5 | browsers: ['last 2 versions', '> 5%'], 6 | }, 7 | }, 8 | }; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const path = require('path'); 4 | 5 | app.use('/static', express.static('dist')); 6 | 7 | app.get('/', function (req, res) { 8 | res.sendFile(path.resolve('./index.html')); 9 | }); 10 | 11 | const address = process.env.IP || "localhost"; 12 | const port = process.env.PORT || 3000; 13 | 14 | app.listen(port, address, (err, result) => { 15 | if (err) { 16 | return console.log(err); 17 | } 18 | console.log(`Chat server listening at ${address}:${port}`); 19 | }); 20 | -------------------------------------------------------------------------------- /src/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /src/__tests__/actions/actionCreators.test.ts: -------------------------------------------------------------------------------- 1 | import { textChanged, inputChanged } from 'actions/actionCreators' 2 | import { ActionType } from 'actions/actionTypes' 3 | 4 | const textValue = 'text' 5 | 6 | describe('textChanged should create a valid action', () => { 7 | const action = textChanged(textValue) 8 | 9 | test('action should equal expected model', () => { 10 | expect(action).toEqual({ 11 | type: ActionType.TEXT_CHANGED, 12 | text: textValue, 13 | }) 14 | }) 15 | }) 16 | 17 | describe('inputChanged should create a valid action', () => { 18 | const action = inputChanged(textValue) 19 | 20 | test('action should equal expected model', () => { 21 | expect(action).toEqual({ 22 | type: ActionType.INPUT_CHANGED, 23 | input: textValue, 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/__tests__/actions/actionTypes.test.ts: -------------------------------------------------------------------------------- 1 | import { ActionType } from 'actions/actionTypes' 2 | 3 | test('every action type should have the own name as string value', () => { 4 | for (const type in ActionType) { 5 | expect(ActionType[type]).toBeDefined() 6 | expect(type).toEqual(ActionType[type] as string) 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /src/__tests__/components/MarkdownText.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | import { MarkdownText } from 'components/MarkdownText' 4 | 5 | describe('MarkdownText shall render text', () => { 6 | const textValue = '# headline\ntext' 7 | 8 | const component = renderer.create() 9 | let tree = component.toJSON() 10 | 11 | test('Snapshot test', () => { 12 | expect(tree).toMatchSnapshot() 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/__tests__/components/TextArea.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as renderer from 'react-test-renderer' 3 | import { TextArea } from 'components/TextArea' 4 | 5 | describe('TextArea shall render text', () => { 6 | const textValue = '# headline\ntext' 7 | const onChange = jest.fn() 8 | const component = renderer.create( 9 |