reactjs - APIコールから受け取ったデータに基づいてReactコンポーネントの初期状態を設定しようとしています
私はReactとTypeScriptを学んでおり、小さなアプリケーション(ハングマンのような単語当てゲームで、5回の試行で単語を当てる)を作っています。このアプリはAPIコールを行い、そこから単語を取得します。データから単語の配列を作成し、その配列からランダムな単語を取り出して、DOMにその単語を表示します。
私は小さなアプリを書きました。それはReactのUIを使った単語当てゲームです。私は定数ファイルに配列のwordsを持っていて、その配列をインポートしていました。その配列を配列からランダムな単語を返すメソッドに渡していました。そして、返された単語をもとに初期状態を設定していました。現在、このアプリを拡張して、APIを呼び出し、APIから返ってきた単語を表示しようとしています。しかし、アプリの初期状態を設定するのに苦労しています。
私は変数 words を作成し、型 any の空の配列に設定しようとしました。その変数をコンストラクタに追加し、componentWillMount フックで API コールを行い、その配列を API コールから返された値で更新しています。その配列を randomWord メソッドに渡して初期状態を設定する単語を取得しようとしていますが、次のエラーメッセージが表示されます
配列が空です
interface LetterType {
letter: string
disabled: boolean
}
interface State {
alphabet: string
letters: LetterType[]
guessedLetters: Set<string>
guessedTheWord: boolean
guessesLeft: number
word: LetterType[]
}
class App extends Component<{}, State> {
words :any[];
constructor(props: {}) {
super(props)
this.state = this.initialState()
this.words = []
this.selectLetter = this.selectLetter.bind(this)
this.startNewGame = this.startNewGame.bind(this)
this.addKeyPressListener()
}
//method to set initial state
initialState(): State {
const alphabet: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const letters: LetterType[] = Array.from(alphabet).map(letter => ({
letter,
disabled: false
}))
//this is were i am getting a error message because when initial state is //called in constructor at that time array is still empty
// I can't figure out a better way to get data here from the api call
const word: LetterType[] = Array.from(this.randomWord(this.words)).map(
letter => ({
letter,
disabled: false
})
)
const guessedLetters: Set<string> = new Set()
const guessesLeft: number = 5
const guessedTheWord: boolean = false
return {
alphabet,
letters,
this.word,
guessedLetters,
guessesLeft,
guessedTheWord
}
}
// making a get request. I was able to log the results from the get request
componentMount(){
axios.get('api/url')
.then(response => response)
.then(
(result) => {
this.words = result.data.split('\na')
},
(error) => {
console.log(error)
}
)
}
//this method is called in the initialState to return the random word
randomWord(words: any[] ): string {
return words[
Math.floor(Math.random() * words.length)
].toUpperCase()
}
selectLetter(selectedLetter: string): void {
this.updateGameStatus(this.state, selectedLetter)
}
updateGameStatus(state: State, selectedLetter: string): void {
if (state.guessesLeft > 0 && !state.guessedTheWord) {
const nextState: State = { ...state }
const alreadyGuessedLetter: boolean = state.guessedLetters.has(
selectedLetter
)
if (!alreadyGuessedLetter) {
nextState.guessedLetters.add(selectedLetter)
const letter: LetterType | undefined = nextState.letters.find(
letter => letter.letter === selectedLetter
)
if (letter) {
letter.disabled = true
}
}
const guessedTheWord: boolean = nextState.word
.filter(letter => !['-', ' '].includes(letter.letter))
.every(letter => nextState.guessedLetters.has(letter.letter))
const lettersFoundInWord: boolean =
nextState.word.filter(letter => letter.letter === selectedLetter)
.length > 0
if (guessedTheWord) {
nextState.guessedTheWord = true
} else if (!alreadyGuessedLetter && !lettersFoundInWord) {
nextState.guessesLeft--
}
nextState.word.forEach(letter =>
letter.letter === selectedLetter ? (letter.disabled = true) : ''
)
this.setState(nextState)
}
}
startNewGame(): void {
this.setState(this.initialState())
}
addKeyPressListener(): void {
document.addEventListener('keydown', event => {
const letter: string = event.key.toUpperCase()
if (this.state.alphabet.includes(letter)) {
this.selectLetter(letter)
}
})
}
render() {
return (
<Fragment>
<Word
word={this.state.word}
guessedTheWord={this.state.guessedTheWord}
/>
<div className="footer">
<GuessesLeft guessesLeft={this.state.guessesLeft} />
<GameResult
guessedTheWord={this.state.guessedTheWord}
guessesLeft={this.state.guessesLeft}
/>
<NewWord onClick={this.startNewGame} />
</div>
<p className="sr-only">
Welcome to play word guess game. Try to guess the word from the given
category. Use keyboard to guess the letters.
</p>
<Alphabet letters={this.state.letters} onClick={this.selectLetter} />
</Fragment>
)
}
}
export default App
配列は空です
回答 #1
コンポーネントがレンダリングされると、コンストラクタが最初に実行されます。コンストラクタではthis.initialState()を呼び出しますが、その時点ではthis.wordsはまだ空の配列です。componentDidMountが実行されると、ご覧の通り必要なデータを取得しますが、このデータ取得は非同期です。データ取得が完了する前に、コンポーネントを「データ読み込み中」の状態でレンダリングする必要があり、取得が完了したら初期化を完了させ、このデータでコンテンツを再レンダリングする必要があります。
まず第一に、あなたのinitialStateメソッドではまだ word を初期化できると期待すべきではありません。代わりに、state のwordプロパティをnullなどに設定してください。その後、renderメソッドでそれを確認し、ユーザーがゲームの読み込みが完了するまで待つ必要があることを知らせる何かをレンダリングする必要があることがわかります:
render() {
if (!this.state.word) {
return <div className="spinner">Loading. Please wait...</div>;
}
// ...
}
これで、フェッチのコールバックを変更してコンポーネントの状態を設定し、初期化を完了して再レンダリングできるようにします:
componentDidMount() {
axios.get('api/url')
.then(
(result) => {
const words = result.data.split('\na');
const word = this.randomWord(words);
this.setState({ word });
},
(error) => {
console.log(error)
}
)
}
回答 #2
あなたの initialState メソッドの戻り値には this.word が含まれています。それは確かに間違いです:
return {
alphabet,
letters,
this.word, // <-- This should just be `word`?
guessedLetters,
guessesLeft,
guessedTheWord
}
あなたはまた、this を initialState メソッドにバインドしていないので、おそらく期待する this に確実にアクセスできません。コンポーネントの this に依存しているメソッドがいくつかありますが、それらはコンストラクタでバインドされていません。
私があなたの立場なら、initialState と randomWord のメソッドをコンポーネントの上に移動して、それらをコンポーネントの一部としてではなくユーティリティ関数として使います。計算を行うために words を渡すことができます。また、updateGameStatus 関数もコンポーネントの上に移動して、データを渡すだけにすることを試してみます。これは必須ではありません、あくまで私の意見です。
私はまた、あなたが this.words を this.state に移動することを提案します。あなたの this.words は 状態 です。それは適切な React の state オブジェクトに属します。
これにより、次の問題が解決されます。componentDidMount の呼び出しで、同期的に this.words を設定しようとしている場合、代わりに React の this.setState メソッドを使用してデータを非同期に保存する必要があります:
componentDidMount() {
axios.get('api/url')
.then(response => response)
.then(
(result) => {
this.setState({
words: result.data.split('\na')
});
},
(error) => {
console.log(error)
}
)
}
これでうまく動かすためにかなり近づくはずです。どうなったか教えてください。頑張ってください。Reactを練習するのに良いプロジェクトです。
