概要
前回、「文字の類似度」からポケモン名をサジェストするコンポーネントを実装しました。
ただ、勢いで作ってしまったため、JavaScriptベースのReactアプリで作ってしまいました。そのため、TypeScriptベースのReactに移行したいと思います。
移行作業
今回の移行方法としては、TypeScriptベースのReactで新規のアプリを新規で作成し、その上に追加開発分を引っ越しします。
1. 新しくTypeScriptベースのReactアプリを作成
下記コマンドで新しくTypeScriptベースのReactアプリを作成します。
名前は何でも良いです。
$ npx create-react-app new-app --template typescript
2. 追加のライブラリをインストール
今回のプロジェクトは、sass
しか追加インストールしていないので直接コマンドをたたきました。
$ yarn add sass
3. 追加開発分ファイルの移行
旧プロジェクトから、新プロジェクトに追加開発分のファイルを持ってきます。拡張子がjsのファイルはtsxに書き換えます。
私のプロジェクトでは以下のファイルのみでした。
旧プロジェクト | 新プロジェクト |
---|---|
App.js | App.tsx |
pokeNameList.js | pokeNameList.tsx |
App.scss | App.scss |
4. JavaScriptをTypeScriptに書き換え
pokeNameList.tsx
は、ただの配列が定義されているだけだったのでファイル名の修正だけで問題ありませんでした。
App.tsxについては、下記の変更を行いました。
一応、ファイルも記載しておきます。
移行前のApp.js
import React, { useState, useEffect } from 'react';
import styles from './App.scss';
import { pokeNameList } from './pokeNameList';
const App = () => {
return <SuggestForm />;
};
const SuggestForm = () => {
const [value, setValue] = useState('');
const suggestNum = 15 ;
const wordList = pokeNameList;
const [suggestions, setSuggestions] = useState([]);
useEffect(() => {
const suggestions = getSuggest(hiraganaToKatakana(value), wordList, suggestNum);
setSuggestions(suggestions);
}, [value, wordList]);
// イベント
const handleChange = (event) => {
const updatedValue = event.target.value;
setValue(updatedValue);
};
/**
* クリックされたワードを検索ボックスにセット
* @param {word} str1 文字列
*/
const handleClickWord = (word) => {
setValue(word);
};
/**
* レーベンシュタイン距離の計測
* @param {string} str1 文字列1
* @param {string} str2 文字列2
* @return {array} レーベンシュタイン距離の計測結果
*/
const levenshteinDistance = (str1, str2) => {
const len1 = str1.length;
const len2 = str2.length;
const dp = [];
for (let i = 0; i <= len1; i++) {
dp[i] = [];
for (let j = 0; j <= len2; j++) {
if (i === 0) {
dp[i][j] = j;
} else if (j === 0) {
dp[i][j] = i;
} else {
dp[i][j] = 0;
}
}
}
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
if (str1[i - 1] === str2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
}
}
}
return dp[len1][len2];
};
/**
* 複数の文字列をレーベンシュタイン距離で評価した結果をソートし返却する
* @param {string} searchWord 検索文字列
* @param {array} wordList 検索対象の一覧
* @param {number} n 返却する文字列の数
* @return {array} 評価順のデータ
*/
const getSuggest = (searchWord, wordList, n) => {
return wordList
.sort((a, b) => levenshteinDistance(searchWord, a) - levenshteinDistance(searchWord, b))
.slice(0, n);
};
/**
* ひらがなをカタカナに変換する
* @param {string} s ひらがな文字列
* @return {string} カタカナ文字列
*/
const hiraganaToKatakana = (s) => {
return s.normalize('NFKC').replace(/[\u3041-\u3096]/g, function(match) {
return String.fromCharCode(match.charCodeAt(0) + 0x60);
});
};
return (
<div>
<div>
<label name="pokemonSearch">ポケモン名 </label>
<input id="pokemonSearch" type="search" value={value} onChange={handleChange} autoComplete="off" />
</div>
<div id="wordList">
<p>もしかして...</p>
{suggestions.map((word, index) => (
<span
key={index}
className="keyword"
onClick={() => handleClickWord(word)}
>
{word}
</span>
))}
</div>
</div>
);
};
export default App;
移行後のApp.tsx
import React, { useState, useEffect } from 'react';
import './App.scss';
import { pokeNameList } from './pokeNameList';
const App = () => {
return <SuggestForm />;
};
const SuggestForm = () => {
const [value, setValue] = useState<string>('');
const suggestNum: number = 15 ;
const wordList: string[] = pokeNameList;
const [suggestions, setSuggestions] = useState<string[]>([]);
useEffect(() => {
const suggestions: string[] = getSuggest(hiraganaToKatakana(value), wordList, suggestNum);
setSuggestions(suggestions);
}, [value, wordList, suggestNum]);
// イベント
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const updatedValue = event.target.value;
setValue(updatedValue);
};
const handleClickWord = (word: string) => {
setValue(word); // クリックされたワードを検索ボックスにセットする
};
/**
* レーベンシュタイン距離の計測
* @param {string} str1 文字列1
* @param {string} str2 文字列2
* @return {array} レーベンシュタイン距離の計測結果
*/
const levenshteinDistance = (str1: string, str2: string) => {
const len1 = str1.length;
const len2 = str2.length;
const dp: number[][] = [];
for (let i = 0; i <= len1; i++) {
dp[i] = [];
for (let j = 0; j <= len2; j++) {
if (i === 0) {
dp[i][j] = j;
} else if (j === 0) {
dp[i][j] = i;
} else {
dp[i][j] = 0;
}
}
}
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
if (str1[i - 1] === str2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
}
}
}
return dp[len1][len2];
};
/**
* 複数の文字列をレーベンシュタイン距離で評価した結果をソートし返却する
* @param {string} searchWord 検索文字列
* @param {array} wordList 検索対象の一覧
* @param {number} n 返却する文字列の数
* @return {array} 評価順のデータ
*/
const getSuggest = (searchWord: string, wordList: string[], n: number) => {
return wordList
.sort((a, b) => levenshteinDistance(searchWord, a) - levenshteinDistance(searchWord, b))
.slice(0, n);
};
/**
* ひらがなをカタカナに変換する
* @param {string} s ひらがな文字列
* @return {string} カタカナ文字列
*/
const hiraganaToKatakana = (s: string) => {
return s.normalize('NFKC').replace(/[\u3041-\u3096]/g, function(match) {
return String.fromCharCode(match.charCodeAt(0) + 0x60);
});
};
return (
<div>
<div>
<label htmlFor="pokemonSearch">ポケモン名 </label>
<input id="pokemonSearch" type="search" value={value} onChange={handleChange} autoComplete="off" />
</div>
<div id="wordList">
<p>もしかして...</p>
{suggestions.map((word, index) => (
<span
key={index}
className="keyword"
onClick={() => handleClickWord(word)}
>
{word}
</span>
))}
</div>
</div>
);
};
export default App;
5. 動作確認
$ yarn start
問題なく動いていますね!
6. Gitへ変更内容をプッシュ
新プロジェクトのgitの紐づけが無い状態になっているので、旧プロジェクトのGitへ紐づけを行います。
- 新プロジェクト内の
.git
フォルダを削除 - 旧プロジェクト内の
.git
フォルダを新プロジェクトにコピーする - 変更内容をコミットする(下記コマンド参照)
$ git add --all $ git commit -m "JavaScript から TypeScriptへ移行" $ git push origin HEAD
終わりに
今回は、JavaScriptベースのReactアプリをTypeScriptベースのReactアプリに移行する方法をまとめました。
アプリの開発量が多いと、こんなに簡単にはいかないと思いますが、参考程度になれば幸いです。
ここまで読んでいたきありがとうございました。