todo-app-js-ts
Use react hook to create a Todo app, and then migrate to typescript.
1. create todo-app-js
I am going to perform below componets for the app, and I am going to use hooks for implement. It will contain useState, useReducer, useEffect, useCallback
- App
 - TodoList
 - TodoInput
 - TodoItem
 - todoReducer
 
1  | yarn create react-app todo-app-js  | 
1  | import React, { useReducer, useEffect, useCallback } from 'react'  | 
1  | import React, { useRef } from 'react'  | 
1  | import React from 'react'  | 
1  | export default function todoReducer(state, action) {  | 
!! Here I chose
useReducerto instead ofuseState. It looks unnecessary, but if we add a new component as middle component between TodoList and TodoItem, then theuseReduceris better thanuseState.
Pretty easy! right? that is javascript version, because I am a Java progromer before, I feel it is weird. I can give any parameters to our function in the writing duration. For example. How do I know action has type and payload if I just in todoReducer file?If I typo the paylaod, it won’t give any error in the writing duration. So next I am going to change to use TypeScript
2. create todo-app-ts
The components are the same with todo-app-js
1  | yarn create react-app todo-app-ts --template=typescript  | 
!! use
--template=typescriptto tellyarn createto create a typescript application.
Create a interfact as types.
1
2mkdir src/types
touch src/types/todoTypes.ts1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21export interface ITodo{
id: number,
content: string,
completed:boolean
}
export interface IState{
todoList: ITodo[]
}
export interface IAction{
type: ACTION_TYPE,
payload: ITodo | ITodo[] | number //payload can be array and number.
}
export enum ACTION_TYPE{
ADD_TODO = 'addTodo',
REMOVE_TODO = 'removeTodo',
TOGGLE_TODO = 'toggleTodo',
INIT_TODOLIST = 'initTodoList'
}Go to
todoReducer.ts1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38import { ACTION_TYPE, IAction, IState, ITodo } from '../types/todoTypes';
export default function todoReducer(state: IState, action: IAction): IState {
const { type, payload } = action;
switch (type) {
case ACTION_TYPE.INIT_TODOLIST:
return {
...state,
todoList: payload as ITodo[]
}
case ACTION_TYPE.ADD_TODO:
return {
...state,
//payload as ITodo is important, otherwise typescript don't know it is todo or id.
todoList: [...state.todoList, payload as ITodo]
}
case ACTION_TYPE.REMOVE_TODO:
return {
...state,
todoList: state.todoList.filter(todo => todo.id !== payload)
}
case ACTION_TYPE.TOGGLE_TODO:
return {
...state,
todoList: state.todoList.map(todo => {
return todo.id === payload ? {
...todo,
completed: !todo.completed
} : {
...todo
}
})
}
default:
return state;
}
}Evey each component should return ReactElement. So we are not use
rfcformatting, we are change to below formation as well1
2
3
4
5
6
7
8
9
10
11
12
13interface IProps{
...
}
const ComponentName:FC<IProps> = ({...Iprops...}) => {
....
return (
<div>
ComponentName
</div>
);
}
export default ComponentName;Here we don’t need move the
IPropstosrc/types/folder, because here theIPropsis a spicific parameter for this component, not a common parameter. For example belowTodoListdoesn’t havePropsand its has noIPropsinterfacte.
1  | import React, { ReactElement, useReducer, useEffect, useCallback } from 'react'  | 
TodoListdid all the logics, we assumeTodoInputandTodoItemare child components.TodoInput 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42import React, { FC, ReactElement, useRef } from 'react'
import { ITodo } from '../types/todoTypes'
interface IProps {
addTodo: (todo: ITodo) => void,
todoList: ITodo[]
}
const TodoInput: FC<IProps> = ({ addTodo, todoList }): ReactElement => {
const inputRef = useRef<HTMLInputElement>(null);
const addItem = (): void => {
const val: string = inputRef.current!.value.trim();
if (val.length) {
const isExist = todoList.find(todo => todo.content === val);
if (isExist) {
alert('already exist!');
return;
}
addTodo({
id: new Date().getTime(),
content: val,
completed: false
})
inputRef.current!.value = '';
}
}
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={addItem}>Add</button>
</div>
)
}
export default TodoInputBecause we have pointed the
props.addTodoasaddTodoin the definition. So here we can just useaddTodo.Go to
TodoItem1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import React, { FC, ReactElement } from 'react'
import { ITodo } from '../types/todoTypes'
interface IProps {
todo: ITodo,
toggleTodo: (id: number) => void,
removeTodo: (id: number) => void
}
const TodoItem: FC<IProps> = ({ todo, toggleTodo, removeTodo }): ReactElement => {
const { id, content, completed } = todo;
return (
<div>
<input type="checkbox" checked={completed} onChange={() => toggleTodo(id)} />
<span style={{ textDecoration: completed ? 'line-through' : 'none' }}>{content}</span>
<button onClick={() => removeTodo(id)}>Delete</button>
</div>
)
}
export default TodoItem;
3. setup environtment from 0
- Initialize 
todo-app-self1
2
3mkdir todo-app-self
cd todo-app-self
yarn init 
1  | {  | 
If use yarn init -y then the package.json will without author and description.
1  | mkdir src  | 
- Add typescript and tslint globallythis will generate a
1
2yarn global add typescript tslint
tsc --inittsconfig.jsonin the root projct.!!
yarn global add xxxis not equalyarn add global xxx1
2
3yarn add webpack webpack-cli webpack-dev-server
mkdir build
touch build/webpack.config.jsAccording to thiswebpack.config.js 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43cconst path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
entry: './src/index.tsx',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
devtool: process.env.NODE_ENV === 'production' ? false : 'inline-source-map',
devServer: {
client: {
overlay: {
errors: true,
warnings: false,
},
},
static: path.join(__dirname, 'dist'),
hot: true,
compress: false,
host: 'localhost',
port: 3000
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/template/index.html'
})
]
}webpack.config.js,we also need to add files as below:template file content is1
2
3
4
5
6yarn add path html-webpack-plugin clean-webpack-plugin ts-loader
mkdir src/template
touch src/template/index.html
yarn add typescriptindex.html 1
2
3
4
5
6
7
8
9
10
11
12
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo-App-Self</title>
</head>
<body>
<div id="root"></div>
</body>
</html> 
Go back package.json, and add scripts
1  | "scripts": {  | 
Here we used cross-env, so 
1  | yarn add cross-env  | 
Go to index.tsx
1  | console.log('Hello, Webpack')  | 
by now, when run below command, you will see Hello, Webpack in console.
1  | yarn start  | 
Because we need react, it also need add following libraries.
1  | yarn add react react-dom @types/react @types/react-dom  | 
The package.json looks like as below:
1  | {  | 
Most of content in tsconfig.json doesn’t need to modify, but only one jsx from preserve to react
1  | "jsx": "react",  | 
In my experience, it looks like
1  | {  | 
webpack official site is here, if has any problem, the better thing is to visit official site to get guideline.
- Copy todo-app-ts files to this project. and it will has same result.