feat(redux-devtools): convert todomvc example to TypeScript (#618)

This commit is contained in:
Nathan Bierema 2020-08-27 22:01:17 -04:00 committed by GitHub
parent f1e3f4f834
commit 37191e46e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 382 additions and 198 deletions

View File

@ -13,6 +13,7 @@
"@typescript-eslint/eslint-plugin": "^3.9.0", "@typescript-eslint/eslint-plugin": "^3.9.0",
"@typescript-eslint/parser": "^3.9.0", "@typescript-eslint/parser": "^3.9.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"eslint": "^7.6.0", "eslint": "^7.6.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",
"eslint-plugin-babel": "^5.3.1", "eslint-plugin-babel": "^5.3.1",
@ -22,7 +23,9 @@
"jest": "^26.2.2", "jest": "^26.2.2",
"lerna": "^3.22.1", "lerna": "^3.22.1",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"raw-loader": "^4.0.1",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"style-loader": "^1.2.1",
"ts-jest": "^26.2.0", "ts-jest": "^26.2.0",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"typescript": "^3.9.7", "typescript": "^3.9.7",

View File

@ -1,7 +1,4 @@
{ {
"extends": "../../../tsconfig.react.base.json", "extends": "../../../tsconfig.react.base.json",
"compilerOptions": {
"outDir": "lib"
},
"include": ["src"] "include": ["src"]
} }

View File

@ -1,7 +1,4 @@
{ {
"extends": "../../../../tsconfig.react.base.json", "extends": "../../../../tsconfig.react.base.json",
"compilerOptions": {
"outDir": "lib"
},
"include": ["src"] "include": ["src"]
} }

View File

@ -1,8 +1,11 @@
{ {
"presets": ["@babel/preset-env", "@babel/preset-react"], "presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": [ "plugins": [
"@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-function-bind",
"react-hot-loader/babel" "react-hot-loader/babel"
] ]
} }

View File

@ -0,0 +1,21 @@
module.exports = {
extends: '../../../../.eslintrc',
overrides: [
{
files: ['*.ts', '*.tsx'],
extends: '../../../../eslintrc.ts.react.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
},
{
files: ['webpack.config.ts'],
extends: '../../../../eslintrc.ts.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.webpack.json'],
},
},
],
};

View File

@ -1,42 +0,0 @@
import * as types from '../constants/ActionTypes';
export function addTodo(text) {
return {
type: types.ADD_TODO,
text,
};
}
export function deleteTodo(id) {
return {
type: types.DELETE_TODO,
id,
};
}
export function editTodo(id, text) {
return {
type: types.EDIT_TODO,
id,
text,
};
}
export function markTodo(id) {
return {
type: types.MARK_TODO,
id,
};
}
export function markAll() {
return {
type: types.MARK_ALL,
};
}
export function clearMarked() {
return {
type: types.CLEAR_MARKED,
};
}

View File

@ -0,0 +1,82 @@
import * as types from '../constants/ActionTypes';
interface AddTodoAction {
type: typeof types.ADD_TODO;
text: string;
}
export function addTodo(text: string): AddTodoAction {
return {
type: types.ADD_TODO,
text,
};
}
interface DeleteTodoAction {
type: typeof types.DELETE_TODO;
id: number;
}
export function deleteTodo(id: number): DeleteTodoAction {
return {
type: types.DELETE_TODO,
id,
};
}
interface EditTodoAction {
type: typeof types.EDIT_TODO;
id: number;
text: string;
}
export function editTodo(id: number, text: string): EditTodoAction {
return {
type: types.EDIT_TODO,
id,
text,
};
}
interface MarkTodoAction {
type: typeof types.MARK_TODO;
id: number;
}
export function markTodo(id: number): MarkTodoAction {
return {
type: types.MARK_TODO,
id,
};
}
interface MarkAllAction {
type: typeof types.MARK_ALL;
}
export function markAll(): MarkAllAction {
return {
type: types.MARK_ALL,
};
}
interface ClearMarkedAction {
type: typeof types.CLEAR_MARKED;
}
export function clearMarked(): ClearMarkedAction {
return {
type: types.CLEAR_MARKED,
};
}
export type TodoAction =
| AddTodoAction
| DeleteTodoAction
| EditTodoAction
| MarkTodoAction
| MarkAllAction
| ClearMarkedAction;
export interface TodoActions {
addTodo(text: string): AddTodoAction;
deleteTodo(id: number): DeleteTodoAction;
editTodo(id: number, text: string): EditTodoAction;
markTodo(id: number): MarkTodoAction;
markAll(): MarkAllAction;
clearMarked(): ClearMarkedAction;
}

View File

@ -1,7 +1,12 @@
import React, { Component } from 'react'; import React, { Component, MouseEventHandler } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { SHOW_ALL, SHOW_MARKED, SHOW_UNMARKED } from '../constants/TodoFilters'; import {
SHOW_ALL,
SHOW_MARKED,
SHOW_UNMARKED,
TodoFilter,
} from '../constants/TodoFilters';
const FILTER_TITLES = { const FILTER_TITLES = {
[SHOW_ALL]: 'All', [SHOW_ALL]: 'All',
@ -9,7 +14,15 @@ const FILTER_TITLES = {
[SHOW_MARKED]: 'Completed', [SHOW_MARKED]: 'Completed',
}; };
export default class Footer extends Component { interface Props {
markedCount: number;
unmarkedCount: number;
filter: TodoFilter;
onClearMarked: MouseEventHandler<HTMLButtonElement>;
onShow: (filter: TodoFilter) => void;
}
export default class Footer extends Component<Props> {
static propTypes = { static propTypes = {
markedCount: PropTypes.number.isRequired, markedCount: PropTypes.number.isRequired,
unmarkedCount: PropTypes.number.isRequired, unmarkedCount: PropTypes.number.isRequired,
@ -23,7 +36,7 @@ export default class Footer extends Component {
<footer className="footer"> <footer className="footer">
{this.renderTodoCount()} {this.renderTodoCount()}
<ul className="filters"> <ul className="filters">
{[SHOW_ALL, SHOW_UNMARKED, SHOW_MARKED].map((filter) => ( {([SHOW_ALL, SHOW_UNMARKED, SHOW_MARKED] as const).map((filter) => (
<li key={filter}>{this.renderFilterLink(filter)}</li> <li key={filter}>{this.renderFilterLink(filter)}</li>
))} ))}
</ul> </ul>
@ -43,7 +56,7 @@ export default class Footer extends Component {
); );
} }
renderFilterLink(filter) { renderFilterLink(filter: TodoFilter) {
const title = FILTER_TITLES[filter]; const title = FILTER_TITLES[filter];
const { filter: selectedFilter, onShow } = this.props; const { filter: selectedFilter, onShow } = this.props;

View File

@ -2,16 +2,20 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import TodoTextInput from './TodoTextInput'; import TodoTextInput from './TodoTextInput';
export default class Header extends Component { interface Props {
addTodo: (text: string) => void;
}
export default class Header extends Component<Props> {
static propTypes = { static propTypes = {
addTodo: PropTypes.func.isRequired, addTodo: PropTypes.func.isRequired,
}; };
handleSave(text) { handleSave = (text: string) => {
if (text.length !== 0) { if (text.length !== 0) {
this.props.addTodo(text); this.props.addTodo(text);
} }
} };
render() { render() {
return ( return (
@ -19,7 +23,7 @@ export default class Header extends Component {
<h1>todos</h1> <h1>todos</h1>
<TodoTextInput <TodoTextInput
newTodo={true} newTodo={true}
onSave={::this.handleSave} onSave={this.handleSave}
placeholder="What needs to be done?" placeholder="What needs to be done?"
/> />
</header> </header>

View File

@ -1,16 +1,32 @@
import React, { Component } from 'react'; import React, { Component, MouseEventHandler } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import TodoItem from './TodoItem'; import TodoItem from './TodoItem';
import Footer from './Footer'; import Footer from './Footer';
import { SHOW_ALL, SHOW_MARKED, SHOW_UNMARKED } from '../constants/TodoFilters'; import {
SHOW_ALL,
SHOW_MARKED,
SHOW_UNMARKED,
TodoFilter,
} from '../constants/TodoFilters';
import { Todo } from '../reducers/todos';
import { TodoActions } from '../actions/TodoActions';
const TODO_FILTERS = { const TODO_FILTERS = {
[SHOW_ALL]: () => true, [SHOW_ALL]: () => true,
[SHOW_UNMARKED]: (todo) => !todo.marked, [SHOW_UNMARKED]: (todo: Todo) => !todo.marked,
[SHOW_MARKED]: (todo) => todo.marked, [SHOW_MARKED]: (todo: Todo) => todo.marked,
}; };
export default class MainSection extends Component { interface State {
filter: TodoFilter;
}
interface Props {
todos: Todo[];
actions: TodoActions;
}
export default class MainSection extends Component<Props, State> {
static propTypes = { static propTypes = {
todos: PropTypes.array.isRequired, todos: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired, actions: PropTypes.object.isRequired,
@ -18,22 +34,19 @@ export default class MainSection extends Component {
// Keep a counter that can be used to create an html `id` attribute. // Keep a counter that can be used to create an html `id` attribute.
static mountCount = 0; static mountCount = 0;
constructor(props, context) { state: State = { filter: SHOW_ALL };
super(props, context); htmlFormInputId = `toggle-all-${MainSection.mountCount++}`;
this.state = { filter: SHOW_ALL };
this.htmlFormInputId = `toggle-all-${MainSection.mountCount++}`;
}
handleClearMarked() { handleClearMarked: MouseEventHandler<HTMLButtonElement> = () => {
const atLeastOneMarked = this.props.todos.some((todo) => todo.marked); const atLeastOneMarked = this.props.todos.some((todo) => todo.marked);
if (atLeastOneMarked) { if (atLeastOneMarked) {
this.props.actions.clearMarked(); this.props.actions.clearMarked();
} }
} };
handleShow(filter) { handleShow = (filter: TodoFilter) => {
this.setState({ filter }); this.setState({ filter });
} };
render() { render() {
const { todos, actions } = this.props; const { todos, actions } = this.props;
@ -58,7 +71,7 @@ export default class MainSection extends Component {
); );
} }
renderToggleAll(markedCount) { renderToggleAll(markedCount: number) {
const { todos, actions } = this.props; const { todos, actions } = this.props;
if (todos.length > 0) { if (todos.length > 0) {
@ -69,6 +82,7 @@ export default class MainSection extends Component {
className="toggle-all" className="toggle-all"
type="checkbox" type="checkbox"
checked={markedCount === todos.length} checked={markedCount === todos.length}
// eslint-disable-next-line @typescript-eslint/unbound-method
onChange={actions.markAll} onChange={actions.markAll}
/> />
<label htmlFor={this.htmlFormInputId}>Mark all as complete</label> <label htmlFor={this.htmlFormInputId}>Mark all as complete</label>
@ -77,7 +91,7 @@ export default class MainSection extends Component {
} }
} }
renderFooter(markedCount) { renderFooter(markedCount: number) {
const { todos } = this.props; const { todos } = this.props;
const { filter } = this.state; const { filter } = this.state;
const unmarkedCount = todos.length - markedCount; const unmarkedCount = todos.length - markedCount;
@ -88,8 +102,8 @@ export default class MainSection extends Component {
markedCount={markedCount} markedCount={markedCount}
unmarkedCount={unmarkedCount} unmarkedCount={unmarkedCount}
filter={filter} filter={filter}
onClearMarked={::this.handleClearMarked} onClearMarked={this.handleClearMarked}
onShow={::this.handleShow} onShow={this.handleShow}
/> />
); );
} }

View File

@ -2,8 +2,23 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import TodoTextInput from './TodoTextInput'; import TodoTextInput from './TodoTextInput';
import { Todo } from '../reducers/todos';
export default class TodoItem extends Component { interface State {
editing: boolean;
}
interface Props {
todo: Todo;
addTodo: (text: string) => void;
deleteTodo: (id: number) => void;
editTodo: (id: number, text: string) => void;
markTodo: (id: number) => void;
markAll: () => void;
clearMarked: () => void;
}
export default class TodoItem extends Component<Props, State> {
static propTypes = { static propTypes = {
todo: PropTypes.object.isRequired, todo: PropTypes.object.isRequired,
editTodo: PropTypes.func.isRequired, editTodo: PropTypes.func.isRequired,
@ -11,18 +26,15 @@ export default class TodoItem extends Component {
markTodo: PropTypes.func.isRequired, markTodo: PropTypes.func.isRequired,
}; };
constructor(props, context) { state: State = {
super(props, context);
this.state = {
editing: false, editing: false,
}; };
}
handleDoubleClick() { handleDoubleClick = () => {
this.setState({ editing: true }); this.setState({ editing: true });
} };
handleSave(id, text) { handleSave(id: number, text: string) {
if (text.length === 0) { if (text.length === 0) {
this.props.deleteTodo(id); this.props.deleteTodo(id);
} else { } else {
@ -52,7 +64,7 @@ export default class TodoItem extends Component {
checked={todo.marked} checked={todo.marked}
onChange={() => markTodo(todo.id)} onChange={() => markTodo(todo.id)}
/> />
<label onDoubleClick={::this.handleDoubleClick}>{todo.text}</label> <label onDoubleClick={this.handleDoubleClick}>{todo.text}</label>
<button className="destroy" onClick={() => deleteTodo(todo.id)} /> <button className="destroy" onClick={() => deleteTodo(todo.id)} />
</div> </div>
); );

View File

@ -1,8 +1,25 @@
import React, { Component } from 'react'; import React, {
ChangeEventHandler,
Component,
FocusEventHandler,
KeyboardEventHandler,
} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
export default class TodoTextInput extends Component { interface State {
text: string;
}
interface Props {
onSave: (text: string) => void;
text?: string;
placeholder?: string;
editing?: boolean;
newTodo?: boolean;
}
export default class TodoTextInput extends Component<Props, State> {
static propTypes = { static propTypes = {
onSave: PropTypes.func.isRequired, onSave: PropTypes.func.isRequired,
text: PropTypes.string, text: PropTypes.string,
@ -11,32 +28,29 @@ export default class TodoTextInput extends Component {
newTodo: PropTypes.bool, newTodo: PropTypes.bool,
}; };
constructor(props, context) { state = {
super(props, context);
this.state = {
text: this.props.text || '', text: this.props.text || '',
}; };
}
handleSubmit(e) { handleSubmit: KeyboardEventHandler<HTMLInputElement> = (e) => {
const text = e.target.value.trim(); const text = e.currentTarget.value.trim();
if (e.which === 13) { if (e.which === 13) {
this.props.onSave(text); this.props.onSave(text);
if (this.props.newTodo) { if (this.props.newTodo) {
this.setState({ text: '' }); this.setState({ text: '' });
} }
} }
} };
handleChange(e) { handleChange: ChangeEventHandler<HTMLInputElement> = (e) => {
this.setState({ text: e.target.value }); this.setState({ text: e.target.value });
} };
handleBlur(e) { handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
if (!this.props.newTodo) { if (!this.props.newTodo) {
this.props.onSave(e.target.value); this.props.onSave(e.target.value);
} }
} };
render() { render() {
return ( return (
@ -49,9 +63,9 @@ export default class TodoTextInput extends Component {
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
autoFocus={true} autoFocus={true}
value={this.state.text} value={this.state.text}
onBlur={::this.handleBlur} onBlur={this.handleBlur}
onChange={::this.handleChange} onChange={this.handleChange}
onKeyDown={::this.handleSubmit} onKeyDown={this.handleSubmit}
/> />
); );
} }

View File

@ -1,3 +1,8 @@
export const SHOW_ALL = 'show_all'; export const SHOW_ALL = 'show_all';
export const SHOW_MARKED = 'show_marked'; export const SHOW_MARKED = 'show_marked';
export const SHOW_UNMARKED = 'show_unmarked'; export const SHOW_UNMARKED = 'show_unmarked';
export type TodoFilter =
| typeof SHOW_ALL
| typeof SHOW_MARKED
| typeof SHOW_UNMARKED;

View File

@ -3,8 +3,15 @@ import React, { Component } from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import TodoApp from './TodoApp'; import TodoApp from './TodoApp';
import DevTools from './DevTools'; import DevTools from './DevTools';
import { Store } from 'redux';
import { TodoState } from '../reducers';
import { TodoAction } from '../actions/TodoActions';
class Root extends Component { interface Props {
store: Store<TodoState, TodoAction>;
}
class Root extends Component<Props> {
render() { render() {
const { store } = this.props; const { store } = this.props;
return ( return (

View File

@ -1,5 +0,0 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./Root.prod');
} else {
module.exports = require('./Root.dev');
}

View File

@ -2,8 +2,15 @@ import { hot } from 'react-hot-loader/root';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import TodoApp from './TodoApp'; import TodoApp from './TodoApp';
import { Store } from 'redux';
import { TodoState } from '../reducers';
import { TodoAction } from '../actions/TodoActions';
class Root extends Component { interface Props {
store: Store<TodoState, TodoAction>;
}
class Root extends Component<Props> {
render() { render() {
const { store } = this.props; const { store } = this.props;
return ( return (

View File

@ -0,0 +1,15 @@
import { Store } from 'redux';
import { TodoState } from '../reducers';
import { TodoAction } from '../actions/TodoActions';
import { ComponentType } from 'react';
interface Props {
store: Store<TodoState, TodoAction>;
}
const Root: ComponentType<Props> =
process.env.NODE_ENV === 'production'
? // eslint-disable-next-line @typescript-eslint/no-var-requires
require('./Root.prod').default
: // eslint-disable-next-line @typescript-eslint/no-var-requires
require('./Root.dev').default;
export default Root;

View File

@ -1,16 +1,28 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators, Dispatch } from 'redux';
import Header from '../components/Header'; import Header from '../components/Header';
import MainSection from '../components/MainSection'; import MainSection from '../components/MainSection';
import * as TodoActions from '../actions/TodoActions'; import * as TodoActions from '../actions/TodoActions';
import {
TodoAction,
TodoActions as TodoActionsType,
} from '../actions/TodoActions';
import { TodoState } from '../reducers';
import { Todo } from '../reducers/todos';
class TodoApp extends Component { interface Props {
todos: Todo[];
actions: TodoActionsType;
}
class TodoApp extends Component<Props> {
render() { render() {
const { todos, actions } = this.props; const { todos, actions } = this.props;
return ( return (
<div> <div>
{/* eslint-disable-next-line @typescript-eslint/unbound-method */}
<Header addTodo={actions.addTodo} /> <Header addTodo={actions.addTodo} />
<MainSection todos={todos} actions={actions} /> <MainSection todos={todos} actions={actions} />
</div> </div>
@ -18,13 +30,13 @@ class TodoApp extends Component {
} }
} }
function mapState(state) { function mapState(state: TodoState) {
return { return {
todos: state.todos, todos: state.todos,
}; };
} }
function mapDispatch(dispatch) { function mapDispatch(dispatch: Dispatch<TodoAction>) {
return { return {
actions: bindActionCreators(TodoActions, dispatch), actions: bindActionCreators(TodoActions, dispatch),
}; };

View File

@ -16,6 +16,7 @@ render(
if (module.hot) { if (module.hot) {
module.hot.accept('./containers/Root', () => { module.hot.accept('./containers/Root', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const RootContainer = require('./containers/Root').default; const RootContainer = require('./containers/Root').default;
render( render(
<AppContainer> <AppContainer>

View File

@ -2,15 +2,6 @@
"name": "todomvc", "name": "todomvc",
"version": "0.0.1", "version": "0.0.1",
"description": "TodoMVC example for redux", "description": "TodoMVC example for redux",
"private": true,
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --open"
},
"repository": {
"type": "git",
"url": "https://github.com/reduxjs/redux-devtools.git"
},
"keywords": [ "keywords": [
"react", "react",
"reactjs", "reactjs",
@ -23,11 +14,22 @@
"flux", "flux",
"todomvc" "todomvc"
], ],
"license": "MIT", "homepage": "https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools/examples/todomvc",
"bugs": { "bugs": {
"url": "https://github.com/reduxjs/redux-devtools/issues" "url": "https://github.com/reduxjs/redux-devtools/issues"
}, },
"homepage": "https://github.com/reduxjs/redux-devtools#readme", "license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/reduxjs/redux-devtools.git"
},
"scripts": {
"start": "webpack-dev-server --open",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"type-check": "tsc --noEmit",
"type-check:watch": "npm run type-check -- --watch"
},
"dependencies": { "dependencies": {
"classnames": "^2.2.6", "classnames": "^2.2.6",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
@ -36,21 +38,18 @@
"react-hot-loader": "^4.12.21", "react-hot-loader": "^4.12.21",
"react-redux": "^7.2.1", "react-redux": "^7.2.1",
"redux": "^4.0.5", "redux": "^4.0.5",
"todomvc-app-css": "^2.3.0"
},
"devDependencies": {
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-function-bind": "^7.10.5",
"@babel/preset-env": "^7.11.0",
"@babel/preset-react": "^7.10.4",
"babel-loader": "^8.1.0",
"raw-loader": "^4.0.1",
"redux-devtools": "^3.6.1", "redux-devtools": "^3.6.1",
"redux-devtools-dock-monitor": "^1.1.4", "redux-devtools-dock-monitor": "^1.1.4",
"redux-devtools-log-monitor": "^2.0.1", "redux-devtools-log-monitor": "^2.0.1",
"style-loader": "^1.2.1", "todomvc-app-css": "^2.3.0"
"webpack": "^4.44.1", },
"webpack-dev-server": "^3.11.0" "devDependencies": {
} "@types/classnames": "^2.2.10",
"@types/prop-types": "^15.7.3",
"@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8",
"@types/react-redux": "^7.1.9",
"@types/webpack-env": "^1.15.2"
},
"private": true
} }

View File

@ -1,5 +1,9 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import todos from './todos'; import todos, { Todo } from './todos';
export interface TodoState {
todos: Todo[];
}
const rootReducer = combineReducers({ const rootReducer = combineReducers({
todos, todos,

View File

@ -6,8 +6,15 @@ import {
MARK_ALL, MARK_ALL,
CLEAR_MARKED, CLEAR_MARKED,
} from '../constants/ActionTypes'; } from '../constants/ActionTypes';
import { TodoAction } from '../actions/TodoActions';
const initialState = [ export interface Todo {
text: string;
marked: boolean;
id: number;
}
const initialState: Todo[] = [
{ {
text: 'Use Redux', text: 'Use Redux',
marked: false, marked: false,
@ -15,7 +22,7 @@ const initialState = [
}, },
]; ];
export default function todos(state = initialState, action) { export default function todos(state = initialState, action: TodoAction) {
switch (action.type) { switch (action.type) {
case ADD_TODO: case ADD_TODO:
return [ return [
@ -49,7 +56,7 @@ export default function todos(state = initialState, action) {
} }
case CLEAR_MARKED: case CLEAR_MARKED:
return state.filter((todo) => todo.marked === false); return state.filter((todo) => !todo.marked);
default: default:
return state; return state;

View File

@ -1,21 +0,0 @@
import { createStore, compose } from 'redux';
import { persistState } from 'redux-devtools';
import rootReducer from '../reducers';
import DevTools from '../containers/DevTools';
const enhancer = compose(
DevTools.instrument(),
persistState(window.location.href.match(/[?&]debug_session=([^&#]+)\b/))
);
export default function configureStore(initialState) {
const store = createStore(rootReducer, initialState, enhancer);
if (module.hot) {
module.hot.accept('../reducers', () =>
store.replaceReducer(require('../reducers').default)
);
}
return store;
}

View File

@ -0,0 +1,29 @@
import { createStore, compose, PreloadedState } from 'redux';
import { persistState } from 'redux-devtools';
import rootReducer, { TodoState } from '../reducers';
import DevTools from '../containers/DevTools';
function getDebugSessionKey() {
const matches = /[?&]debug_session=([^&#]+)\b/.exec(window.location.href);
return matches && matches.length > 0 ? matches[1] : null;
}
const enhancer = compose(
DevTools.instrument(),
persistState(getDebugSessionKey())
);
export default function configureStore(
initialState?: PreloadedState<TodoState>
) {
const store = createStore(rootReducer, initialState, enhancer);
if (module.hot) {
module.hot.accept('../reducers', () =>
// eslint-disable-next-line @typescript-eslint/no-var-requires
store.replaceReducer(require('../reducers').default)
);
}
return store;
}

View File

@ -1,5 +0,0 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./configureStore.prod');
} else {
module.exports = require('./configureStore.dev');
}

View File

@ -1,6 +0,0 @@
import { createStore } from 'redux';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
return createStore(rootReducer, initialState);
}

View File

@ -0,0 +1,8 @@
import { createStore, PreloadedState } from 'redux';
import rootReducer, { TodoState } from '../reducers';
export default function configureStore(
initialState?: PreloadedState<TodoState>
) {
return createStore(rootReducer, initialState);
}

View File

@ -0,0 +1,13 @@
import { PreloadedState, Store } from 'redux';
import { TodoState } from '../reducers';
import { TodoAction } from '../actions/TodoActions';
const configureStore: (
initialState?: PreloadedState<TodoState>
) => Store<TodoState, TodoAction> =
process.env.NODE_ENV === 'production'
? // eslint-disable-next-line @typescript-eslint/no-var-requires
require('./configureStore.prod').default
: // eslint-disable-next-line @typescript-eslint/no-var-requires
require('./configureStore.dev').default;
export default configureStore;

View File

@ -0,0 +1,4 @@
{
"extends": "../../../../tsconfig.react.base.json",
"include": ["."]
}

View File

@ -0,0 +1,4 @@
{
"extends": "../../../../tsconfig.base.json",
"include": ["webpack.config.ts"]
}

View File

@ -1,9 +1,8 @@
var path = require('path'); import * as path from 'path';
var webpack = require('webpack'); import * as webpack from 'webpack';
module.exports = { module.exports = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
devtool: 'eval-source-map',
entry: [ entry: [
'webpack-dev-server/client?http://localhost:3000', 'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server', 'webpack/hot/only-dev-server',
@ -14,11 +13,10 @@ module.exports = {
filename: 'bundle.js', filename: 'bundle.js',
publicPath: '/static/', publicPath: '/static/',
}, },
plugins: [new webpack.HotModuleReplacementPlugin()],
module: { module: {
rules: [ rules: [
{ {
test: /\.js$/, test: /\.(js|ts)x?$/,
loaders: ['babel-loader'], loaders: ['babel-loader'],
exclude: /node_modules/, exclude: /node_modules/,
include: __dirname, include: __dirname,
@ -37,9 +35,14 @@ module.exports = {
}, },
], ],
}, },
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
plugins: [new webpack.HotModuleReplacementPlugin()],
devServer: { devServer: {
historyApiFallback: true, historyApiFallback: true,
hot: true, hot: true,
port: 3000, port: 3000,
}, },
devtool: 'eval-source-map',
}; };

View File

@ -403,14 +403,6 @@
"@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-export-namespace-from" "^7.8.3" "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
"@babel/plugin-proposal-function-bind@^7.10.5":
version "7.10.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.10.5.tgz#62acbdde1c43e7dfae6efc9ddd5bc60920cee719"
integrity sha512-1lYbE2ynV9yN0LCEYCdEBD5pR6GaNkRfjn1z1tWDdWMJgunTFcJBZDJUgiMPcTMqAc3D6Vrm8v2khxjjx6FrCg==
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-function-bind" "^7.10.4"
"@babel/plugin-proposal-json-strings@^7.0.0", "@babel/plugin-proposal-json-strings@^7.10.4": "@babel/plugin-proposal-json-strings@^7.0.0", "@babel/plugin-proposal-json-strings@^7.10.4":
version "7.10.4" version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db"
@ -563,13 +555,6 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-function-bind@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.10.4.tgz#8378d94f3185ddd3008310c15fe0991cb0c85151"
integrity sha512-vF/K9yS0dpPNlT7mXSGhbdpb2f4DaLa/AYYbUqlxOggAug/oseIR1+LgAzwci4iJNlqWNmJ7aQ+llUMYjn9uhw==
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-import-meta@^7.8.3": "@babel/plugin-syntax-import-meta@^7.8.3":
version "7.10.4" version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"