14
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Redux Example の TODO List を TypeScript で作成

Last updated at Posted at 2016-04-17

Redux Example の TODO List を TypeScript で作成

(2016.04.09--2016.04.17 OS X El Capitan‎)

Redux Example の Todo List を typescript で作成しました。
JavaScript, TypeScript と、それらのアプリケーションフレームワークの勉強を目的としました。

準備

以下のコマンドで、必要なプログラム、パッケージをインストールしました。

brew update
brew install node
npm install typescript -g
npm install typings -g
npm install jspm -g

typings と jspm について

  • typings は、typescript で使用する "型定義ファイル" を管理するためのパッケージ。
    javascript には、静的型付けがないため、".d.ts" という拡張子を持つ型定義ファイルを別に用意することで、
    javascript で書かれた外部ライブラリを typescript でも使用できるようにしている。

  • JSPM は、外部ライブラリ呼び出しを簡単にしてくれるパッケージのようだ。

続けて、"todo-list" という名前のフォルダを作成して、そのフォルダに移動し、
npm init と入力して、プロジェクトの設定を行いました。

mkdir todo-list
cd !$
npm init

npm init とすると色々と聞かれましたが、entry point に関する質問を index.js から index.tsx に変更した以外は、
デフォルトのままにしました(今回の場合、src/index.tsx か dist/index.js にするべきだったかもしれません)。

name: (todo-list) 
version: (1.0.0) 
description: 
entry point: (index.js) index.tsx
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 

質問に答えると、"package.json" というアプリケーションの設定ファイルが自動作成されました。
"package.json" の内容は、以下のようになりました。

{
  "name": "todo-list",
  "version": "1.0.0",
  "description": "",
  "main": "index.tsx",
  "dependencies": {},
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

続いて、npm install の --save-dev オプションでローカルにも jspm をインストールし、
jspm init と入力しました。

npm install jspm --save-dev
jspm init

色々と聞かれましたが、transpiler(ES6で書かれたコードをES5のコードへ変換してくれる)の選択に関する質問
を TypeScript とした以外は全てデフォルトのままとしました。

Would you like jspm to prefix the jspm package.json properties under jspm? [yes]:
Enter server baseURL (public folder path) [./]:
Enter jspm packages folder [./jspm_packages]:
Enter config file path [./config.js]:
Configuration file config.js doesn't exist, create it? [yes]:
Enter client baseURL (public folder URL) [/]:
Do you wish to use a transpiler? [yes]:
Which ES6 transpiler would you like to use, Babel, TypeScript or Traceur? [babel]:TypeScript

質問に答えると config.js というファイルが自動生成され、以下のような内容となりました。

System.config({
  baseURL: "/",
  defaultJSExtensions: true,
  transpiler: "typescript",
  paths: {
    "github:*": "jspm_packages/github/*",
    "npm:*": "jspm_packages/npm/*"
  },

  map: {
    "typescript": "npm:typescript@1.8.9",
    "github:jspm/nodelibs-os@0.1.0": {
      "os-browserify": "npm:os-browserify@0.1.2"
    },
    "npm:os-browserify@0.1.2": {
      "os": "github:jspm/nodelibs-os@0.1.0"
    },
    "npm:typescript@1.8.9": {
      "os": "github:jspm/nodelibs-os@0.1.0"
    }
  }
});

続いて、jspm を使用して react, react-dom, redux, react-redux をインストールして、
typings を使用してそれらの型定義ファイルをインストールしました。

jspm install react
jspm install react-dom
jspm install redux
jspm install react-redux
typings install react --ambient --save
typings install react-dom --ambient --save
typings install redux --ambient --save
typings install react-redux --ambient --save

最後に、mkdir コマンドで、以下のようなフォルダ構造を作成しました。

tree todo-list/src --charset=xyz
todo-list/src
|-- actions
|-- components
|-- constants
|-- containers
`-- reducers

以下のサイトなどを参考にさせていただきました

tsconfig.json の作成

Reux Example の Todo List を作成する前に、
typescript 用に tsconfig.json という設定ファイルを作成しました。

ファイルの作成には、Atom エディタを使用しました。
Atom を起動して、メニューバーの "Atom" から "Install Shell Commands" を選択した後、ターミナルで
apm install atom-typescript として、atom-typescript パッケージをインストールしました。

todo-list/tsconfig.json には、主に以下のような内容を指定しました。

tsconfig.json
{
    "compilerOptions": {
        "target": "es5",
        "sourceMap": true,
        "declaration": true,
        "module": "system",
        "jsx": "react",
        "noImplicitAny": true,
        "noEmitOnError": true,
        "removeComments": true,
        "experimentalDecorators": true
    },
    "compileOnSave": false,
    "filesGlob": [
        "./typings/main.d.ts",
        "./src/*.tsx",
        "./src/**/*.ts",
        "./src/**/*.tsx"
    ],
    "files": [
        "./typings/main.d.ts"
    ],
    "atom": {
        "rewriteTsconfig": true
    }
}

TODO List アプリ

Redux ExampleのTodo Listをはじめからていねいに(1)
などを参考にさせていただき、todo-list/src 以下に TODO List アプリを作成していきました。

上記のページにあるように TODO List アプリには、以下の3つの機能があるようでした。

  • Todo を Todo List に追加する「Add Todo」
  • Todo の完了・未完了を切り替える「Toggle Todo」
  • 表示する Todo List を完了または未完了の Todo だけにする「Filter Todo」

Action

Redux では、ユーザー操作などによって、アプリの状態(Store に格納される)や View を変化させるのに、
まず Action と呼ばれる オブジェクト を発行するようでした;
イベントを受け取ると、以下のように「一方向の流れでページの View を遷移させていく」ようでした。

イベント が発生する
-> Action が作成される
-> Dispatcher が Action を発行する
-> Store に格納されたアプリの状態(State)が変わる
-> State を View へ反映させるとページの見た目が変わる

Action には、type と呼ばれるプロパティ(Action の id 的なもの)を持たせるというルールがあるようでした。
そこで、まずアプリの機能に必要となる Action type を constants/index.ts に列挙しました。

constants/index.ts
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';

続いて、actions/index.ts に以下の3つの関数(Action Creator と呼ばれる 関数)を作成しました。

  • テキストを引数に取り、type が 'ADD_TODO' である Action を作成する関数
  • id を引数に取り、type が 'TOGGLE_TODO' である Action を作成する関数
  • filter("SHOW_ALL" or "SHOW_ACTIVE" or "SHOW_COMPLETED")を引数に取り、type が 'SET_VISIBILITY_FILTER' である Action を作成する関数
actions/index.ts
import * as types from '../constants/index';

let nextTodoId = 0
export const addTodo = (text: string) => {
  return {
    type: types.ADD_TODO,
    id: nextTodoId++,
    text
  }
}

export const toggleTodo = (id: number) => {
  return {
    type: types.TOGGLE_TODO,
    id
  }
}

export const setVisibilityFilter = (filter: string) => {
  return {
    type: types.SET_VISIBILITY_FILTER,
    filter
  }
}

Reducer

状態の設計

Reducer には、発行された Action によって状態がどう遷移するのかを記述するようでした。
そこで、まずアプリの状態を表現するオブジェクトをデザインしました。

State を表現する オブジェクト の型は以下のようになりそうでした。
{ State: {visibilityFilter: string, todos: {id:number, text:string, completed:boolean}[]} }

つまり、例えば以下のような感じになりそうでした(Redux Read Me の Reducers を参考にしました)。

{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      id: 0,
      text: 'Consider using Redux',
      completed: true,
    },
    {
      id: 1,
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}

TODO List アプリの3つの機能と状態オブジェクトの関係を考えてみたところ、
アプリの機能と良くマッチしていそうでした。

  • Todo を Todo List に追加する「Add Todo」
    addTodo アクションによって、todos プロパティの値(配列型)に新たな todo オブジェクトが追加される。
    新規に作成される todo は、どの todo なのかを指定するための id と、 todo の内容を記述した text と、
    その todo が完了したか未完了かを表す completed というプロパティを持つ(初期値は、未完了のため false)。

  • Todo の完了・未完了を切り替える「Toggle Todo」
    各々の todo には、completed というプロパティがあり、
    toggleTodo アクションで指定された id に対応する todo は、値が true となり完了した状態となる
    or 既に完了していた場合、値が false となり未完了の状態になる。

  • 表示する Todo List を完了または未完了の Todo だけにする「Filter Todo」
    setVisibilityFilter アクションによって、visibilityFilter プロパティの値(初期値は、"SHOW_ALL")が、
    "SHOW_ALL" or "SHOW_ACTIVE" or "SHOW_COMPLETED" のいずれかになり、
    それに対応して View に表示する todo を切り替えるロジックを作成することができる。

Reducer の作成時の注意点

状態が設計できたため、Reducer の作成をしていきました。
Reducer は、状態(の一部)と Action を引数として取り、
Action により状態がどう遷移するかをプログラムし、
最後に遷移後の状態を return するという 関数 のようでした。

Reducer は、現在の状態を次の状態へ遷移させますが、変化させる方法には重要なポイントがあり、
現在の状態を書き換えて、次の状態を作り出すのではなく、
現在の状態を不変(immutable)のまま、次の状態を作り出すことのようでした。

なぜこのようにするかというと、理由は良くわかりませんでしたが、
Full-Stack Redux Tutorial のサイトによると、
現在の状態を不変にすることで、テストが書きやすくなるといったメリットがあるようでした。

Redux Read Me の Reducers に、reducer でやってはいけないことが列記されていました。

Mutate its arguments;
Perform side effects like API calls and routing transitions;
Calling non-pure functions, e.g. Date.now() or Math.random().

現在の状態を不変にするには、Object.assign({}, state, {key: value}) という形で、
次の状態を作り出すと良いようでした。こうすると、現在の状態を変えずに、
3つ目の引数の key で指定したプロパティの値だけが変化した新しいオブジェクトが作成されるようでした。

Typescript でも Object.assign() は使用できるようでしたが、
ServiceStackApps/typescript-redux によると、
Typescript で target に ES5 を指定すると Object.assign() が使用できないため、
jspm で es6-shim パッケージを導入すると良いとのことでした。
このため、es6-shim パッケージと型定義ファイルのインストールを行いました。

jspm install es6-shim
typings install es6-shim --ambient --save

Reducer の作成

Example の Reducer は、typescript で書くと以下のようになりました。
Reducer は、アプリの機能(Action)と、アプリの状態(State)の対応関係を考えることで、どのような実装となるのかを考えることができそうでした。

reducers/todos.ts
const todo = (state: any, action: any) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        id: action.id,
        text: action.text,
        completed: false
      }
    case 'TOGGLE_TODO':
      if (state.id !== action.id) {
        return state
      }

      return Object.assign({}, state, {
        completed: !state.completed
      })

    default:
      return state
  }
}

const todos = (state: any[] = [], action: any) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        todo(undefined, action)
      ]
    case 'TOGGLE_TODO':
      return state.map(t =>
        todo(t, action)
      )
    default:
      return state
  }
}

export default todos
reducers/visibilityFilter.ts
const visibilityFilter = (state = 'SHOW_ALL', action: any) => {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

export default visibilityFilter

最後に、reducers/index.ts という、reducer をまとめる役割を持つファイルを作成しました。
Redux では、各々の reducer で別々に扱っていた状態の一部を、combineReducers() を使用することで、
1つの状態を表すオブジェクトにまとめる必要があるようでした。

reducers/index.ts
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'

const todoApp = combineReducers({
  todos,
  visibilityFilter
})

export default todoApp

Components

Redux ExampleのTodo Listをはじめからていねいに(1) の動作イメージの開発者ツール などを見て、
TODO List アプリに、どういう Components があり、どういう構造をしていて、それぞれ何をしているかを調べました。

  • TODO List アプリの HTML の構造
- Provider
  - App
    - Connect(AddTodo)
      - AddTodo
    - Connect(TodoList)
      - TodoList
        - Todo
        - Todo
        - ...
    - Footer
      - Connect(Link): "SHOW_ALL"
      - Connect(Link): "SHOW_ACTIVE"
      - Connect(Link): "SHOW_COMPLETED"
  • TODO List アプリのそれぞれの Components の役割

    • Provider: これで囲むと、react components が redux とやり取りするために connect() を使用できるようになる。
    • App: アプリケーションのルートに相当する components。
    • AddTodo: todo を state に加える役割。redux とやり取りをする。
    • TodoList: todo の管理。visibilityFilter の状態によって表示する todo を決める。また管理する todo がクリックされると completed かどうかを切り替えるために redux とやり取りをする。
    • Todo: todo の表示。todo が completed かどうかを切り替える役割。completed な TODO に横線を引くという表示の切り替えは自分で実行するが、状態の切り替えは TodoList を介して行うため、直接 redux とやり取りしない。
    • Footer: "SHOW_ALL", "SHOW_ACTIVE", "SHOW_COMPLETED" リンクを表示する役割。直接 redux とやり取りしない。
    • Link: クリックされると、現在の visibilityFilter の状態を切り替える。直接 redux とやり取りをする。

Connect() で囲まれた Components と、そうでない Components がある点が気になりました。
Redux Read Me の Usage with React によると、
Components は、主に以下の2つに分かれるようでした。

  • Redux と直接やり取りをしないもの (Presentational Components)
  • Redux と直接やり取りをするもの (Container Components)

直接 redux をやり取りするものには、Connect() と付くようでした。
実際の Example のコード を見てみると、
redux と直接やり取りをする Components でも、
Presentational Components, Container Components に分離することができるようでした。

TodoList

TodoList は、Presentational Components, Container Components に以下のように分離されていました。

Presentational Components

Todo.tsx

  • TodoList を介して、onClick 関数をプロパティとして受け取り、クリックされた時に TOGGLE_TODO Action が発行されるようにしている。
  • クリックされた時に、local な状態を参照して、text に横棒を引いたり、消したりする。
  • TodoList を介して、text をプロパティとして受け取り、表示させる。

TodoList.tsx

  • 表示させる todo のリストを VisibleTodoList Container を介してプロパティとして受け取り、表示させる。
  • Todo にプロパティとして、key (id), id, completed, text, onClick関数 を渡している(プロパティのバケツリレーをしている)。
  • completed かどうかを切り替える関数をプロパティとして受け取り、VisibleTodoList Container を介して状態を切り替える。
components/Todo.tsx
import React, { PropTypes } from 'react'

export interface TodoProps extends React.Props<any> {
  onClick: any;
  completed: boolean;
  text: string;
}

export default class Todo extends React.Component<TodoProps, any> {
  render() {
    return (
      <li
        onClick={this.props.onClick}
        style={ {
            textDecoration: this.props.completed ? 'line-through' : 'none'
        } }
      >
        {this.props.text}
      </li>
    );
  }
}
components/TodoList.tsx
import React, { PropTypes } from 'react'
import Todo from './Todo'

export interface TodoListProps extends React.Props<any> {
  todos: {id: number, completed: boolean, text: string}[],
  onTodoClick: any
}

export default class TodoList extends React.Component<TodoListProps, any> {
  render() {
    return (
      <ul>
        {this.props.todos.map(todo =>
          <Todo
            key={todo.id}
            {...todo}
            onClick={() => this.props.onTodoClick(todo.id)}
          />
        )}
      </ul>
    )
  }
}
Container Components

VisibleTodoList.ts

  • import { connect } from 'react-redux' で redux とやり取りするため connect を使用出来るようにしている。
  • State を利用して、表示する todo を決めて、プロパティに変換して、TodoList に connect している(View 側を変更するため)。
  • TOGGLE_TODO Action を dispatch する関数をプロパティに変換して、TodoList に connect している(State 側を変更するため)。
containers/VisibleTodoList.ts
import { connect } from 'react-redux'
import { toggleTodo } from '../actions/index'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

Link

Link は、Presentational Components, Container Components に以下のように分離されていました。

Presentational Components

Footer.tsx

  • FooterLink にプロパティ(key は filter)として、"SHOW_ALL", "SHOW_ACTIVE", "SHOW_COMPLETED" を渡している(別々に計3回渡している)。
  • FooterLink にプロパティ(key は children)として、"All", "ACTIVE", "COMPLETED" を渡している(別々に計3回渡している)。
  • 3つの FooterLink を介して、Link を表示する。

Link.tsx

  • active かどうかを FooterLink Container を介してプロパティとして受け取り、リンクの有効・無効を切り替える。
  • FooterLink Container を介して、"All", "ACTIVE", "COMPLETED" という文字列のどれかをプロパティとして受け取り、表示する。
    (Footer.tsx の の中身が、children という key に自動で割り当てられている。react の props.children
  • FooterLink Container を介して onClick関数をプロパティとして受け取り、クリックされた時に SET_VISIBILITY_FILTER Action が発行されるようにしている。
components/Footer.tsx
import React from 'react'
import FilterLink from '../containers/FilterLink'

const Footer = () => (
  <p>
    Show:
    {" "}
    <FilterLink filter="SHOW_ALL">
      All
    </FilterLink>
    {", "}
    <FilterLink filter="SHOW_ACTIVE">
      Active
    </FilterLink>
    {", "}
    <FilterLink filter="SHOW_COMPLETED">
      Completed
    </FilterLink>
  </p>
)

export default Footer
components/Link.tsx
import React, { PropTypes } from 'react'

export interface LinkProps extends React.Props<any> {
  active: boolean,
  children: any,
  onClick: any
}

export default class Link extends React.Component<LinkProps, any> {
  if (active) {
    return <span>{this.props.children}</span>
  }
  
  render() {
    return (
      <a href="#"
         onClick={e => {
           e.preventDefault()
           this.props.onClick()
         }}
      >
        {this.props.children}
      </a>
    )
  }
}
Container Components

FooterLink.ts

  • import { connect } from 'react-redux' で redux とやり取りするため connect を使用出来るようにしている。
  • ownProps には、Footer Component から受け取ったプロパティが入っている。
  • State を利用して、自身が active かどうかを切り替え、プロパティに変換して、Link に connect している。
  • SET_VISIBILITY_FILTER Action をプロパティに変換して、Link に connect している。
containers/FooterLink.ts
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions/index'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => {
  return {
    active: ownProps.filter === state.visibilityFilter
  }
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    onClick: () => {
      dispatch(setVisibilityFilter(ownProps.filter))
    }
  }
}

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

export default FilterLink

AddTodo

AddTodo は、Presentational Component, Container Component に分離されずに書かれていました。

AddTodo.tsx

  • import { connect } from 'react-redux' で redux とやり取りするため connect を使用出来るようにしている。
  • ref によって、input 変数が自分自身を参照するようにしている。
  • AddTodo がクリックされると、input.value で入力された値を取得して、値が空の時は何もしない。空でない場合は、ADD_TODO Action が発行されるようにしている。
containers/AddTodo.tsx
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions/index'

let AddTodo = ({ dispatch }):any => {
  let input

  return (
    <div>
      <form onSubmit={e => {
        e.preventDefault()
        if (!input.value.trim()) {
          return
        }
        dispatch(addTodo(input.value))
        input.value = ''
      }}>
        <input ref={node => {
          input = node
        }} />
        <button type="submit">
          Add Todo
        </button>
      </form>
    </div>
  )
}
AddTodo = connect()(AddTodo)

export default AddTodo

App

App Component は、全ての Components をまとめる役割があるようでした。
Container Component で connect() した Presentational Component は、自動で子要素として呼び出されるようで、
App Component で、以下のような構造を作るには、

- Provider
  - App
    - Connect(AddTodo)
      - AddTodo
    - Connect(TodoList)
      - TodoList
        - Todo
        - Todo
        - ...
    - Footer
      - Connect(Link): "SHOW_ALL"
      - Connect(Link): "SHOW_ACTIVE"
      - Connect(Link): "SHOW_COMPLETED"

以下の 3つを App.tsx から呼び出せば良さそうでした。

<AddTodo />
<VisibleTodoList />
<Footer />

コードは、以下のようになりました。

components/App.tsx
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'

const App = () => (
  <div>
    <AddTodo />
    <VisibleTodoList />
    <Footer />
  </div>
)

export default App

Provider

最後に App Component を Provider で囲み、TODO List アプリを挿入する HTML の場所を指定するようでした。
id="root" の場所に挿入することにしました。また、Store を Provider に渡してやるというルールがあるようでした。
以下のように src/index.tsx を作成しました。

index.tsx
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers/index'
import App from './components/App'

let store = createStore(todoApp)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

Compile、Build

以下のコマンドでプロジェクトを dist というディレクトリにコンパイルしました。

tsc --project todo-list --outDir todo-list/dist

また、外部ライブラリをまとめたファイルを作成するため、jspm でビルドを行いました。

jspm bundle-sfx --minify dist/index

index.html の作成

todo-list/index.html を以下のように作成しました。

<html>
<head>
    <title>Redux Todo Example</title>
</head>
<body>
    <h1>TODO List App</h1>
    <div id="root"></div>
    <script src="build.js"></script>
</body>
</html>

最終的なプロジェクトのディレクトリ構造を
tree todo-list -L 2 --charset=xyz として、調べました。

todo-list
|-- build.js
|-- build.js.map
|-- config.js
|-- dist
|   |-- actions
|   |-- components
|   |-- constants
|   |-- containers
|   |-- index.d.ts
|   |-- index.js
|   |-- index.js.map
|   `-- reducers
|-- index.html
|-- jspm_packages
|   |-- ...
|   |-- ...
|-- node_modules
|   |-- ...
|   |-- ...
|-- package.json
|-- src
|   |-- actions
|   |-- components
|   |-- constants
|   |-- containers
|   |-- index.tsx
|   `-- reducers
|-- tsconfig.json
`-- typings.json

GitHub への投稿と GitHub Pages の作成

無料で使える!GitHub Pagesを使ってWebページを公開する方法
参考にさせていただき、TODO List アプリを GitHub Pages にアップしました。

.gitignore を以下のように作成しました(config.js は除かない方が良かったかもしれません。。)。

.gitignore
config.js
dist
jspm_packages
node_modules
typings
.DS_Store

GitHub にログインして、ブラウザで todo-list というレポジトリを作成して、
以下のコマンドにより push しました。

git init
git add .
git commit -m 'first'
git remote add origin https://github.com/UserName/todo-list.git
git push -u origin master

GitHub にログインして、ブラウザで UserName.github.io という名前のリポジトリを作成しました。
続いて、ブラウザで todo-list プロジェクトに gh-pages というブランチを作成しました。

作成された GitHub Pages の TODO List アプリ にアクセスすると、ちゃんと使用することができました。

14
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?