質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.21%

[React]配列の要素が空のとき、要素をpushした後先頭のオブジェクトを削除したい

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 103

moyong

score 3

前提・実現したいこと

React Hooks, Tyoescriptで配列の要素が空であれば上書きし、既にあれば末尾に追加する処理を書きたい。
配列やオブジェクトの扱い方について理解が浅く、ご教授いただきたいです。

問題

stateで持っているオブジェクト(トレーニング名、重さ、回数)

const [ trainingRecord, setTrainingRecord ] = useState({
    trainingName: "",
    trainingWeight: "none",
    trainingReps: "0",
})

を次の配列にpushして、shift()で先頭を削除する処理を書いてみたのですが

let trainingRecords = [{}];
const saveTrainingRecord = () => {
    if (!trainingRecords.length) {
      trainingRecords.push(trainingRecords);
      trainingRecords.shift();
    } else {
      trainingRecords.push(trainingRecord);
    }
    console.log(trainingRecords);
};


イメージ説明
上の画像のコンソールにあるように、0番目の{}という不要なものが削除できていません。
(次々とオブジェクトを追加していくことはできています。)
そもそものロジックについても、こうした方がいいというのがあれば教えていただきたいです。

備考

trainingRecordsという配列(連想配列というのが正確なんでしょうか?)は、
将来的にfirebaseに保存して、Twitterのタイムラインのように一つずつ取り出して一覧表示させる予定です。

trainigRecords = [
 {
  trainingName: "腕立て伏せ",
  trainingWeight: "none",
  trainingReps: "20",
 },
 {
  trainingName: "スクワット",
  trainingWeight: "100kg",
  trainingReps: "5",
 },
]


みたいな構造のtrainingRecordsが、Twitterでいう一つのつぶやきみたいになるようなイメージです。
わざわざこういうデータ構造にしているのは、1日のトレーニングの中で複数のトレーニングを色々な重量で複数セットやる場合を考慮しているためです。

念の為、このコンポーネントを全て貼り付けておきます。

import React, { useState } from "react";
import styles from "./PostInput.module.scss";
import { storage, db, auth } from "../firebase";
import firebase from "firebase/app";
import { useSelector } from "react-redux";
import { selectUser } from "../features/userSlice";
import AddPhotoAlternateIcon from '@material-ui/icons/AddPhotoAlternate';
import AddCircleIcon from '@material-ui/icons/AddCircle';
import {
  Avatar,
  Button,
  IconButton,
  TextField,
  MenuItem,
  createStyles,
  makeStyles,
  Theme,
} from "@material-ui/core";

interface trainingRecords {
  trainingName: string;
  trainingWeight: string;
  trainingReps: string;
}
let trainingRecords = [{}];
const weightList = [
  {value: 'none', label: 'none'},
  {value: '10', label: '10lbs | 4.5kg'},
  {value: '20', label: '20lbs | 9kg'},
  {value: '30', label: '30lbs | 14kg'},
  {value: '40', label: '40lbs | 18kg'},
  {value: '50', label: '50lbs | 23kg'},
  {value: '60', label: '60lbs | 27kg'},
  {value: '70', label: '70lbs | 32kg'},
  {value: '80', label: '80lbs | 36kg'},
  {value: '90', label: '90lbs | 41kg'},
  {value: '100', label: '100lbs | 45kg'},
  {value: '110', label: '110lbs | 50kg'},
  {value: '120', label: '120lbs | 54kg'},
  {value: '130', label: '130lbs | 59kg'},
  {value: '140', label: '140lbs | 64kg'},
  {value: '150', label: '150lbs | 68kg'},
  {value: '160', label: '160lbs | 73kg'},
  {value: '170', label: '170lbs | 77kg'},
  {value: '180', label: '180lbs | 82kg'},
  {value: '190', label: '190lbs | 86kg'},
  {value: '200', label: '200lbs | 91kg'},
];

// const useStyles = makeStyles((theme: Theme) =>
//   createStyles({
//     trainingNameInput: {
//       '& > *': {
//         margin: theme.spacing(1),
//         width: '25ch',
//       },
//     },
//     trainingWeightSelect: {
//       '& .MuiTextField-root': {
//         margin: theme.spacing(1),
//         width: '25ch',
//       },
//     },
//   }),
// );
const TweetInput: React.FC = () => {
  const user = useSelector(selectUser);
  // const classes = useStyles();
  const [ image, setImage] = useState<File | null>(null);
  const [ trainingRecord, setTrainingRecord ] = useState({
    trainingName: "",
    trainingWeight: "none",
    trainingReps: "0",
  })
  const onChangeImageHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files![0]) {
      setImage(e.target.files![0]);
      e.target.value = "";
    }
  };
  const saveTrainingRecord = () => {
    if (!trainingRecords.length) {
      trainingRecords.push(trainingRecords);
      trainingRecords.shift();
    } else {
      trainingRecords.push(trainingRecord);
    }
    setTrainingRecord({
      trainingName: "",
      trainingWeight: "none",
      trainingReps: "0",
    });
    console.log(trainingRecords);
  };
  // const sendTrainingPost = (e: React.FormEvent<HTMLFormElement>) => {
  //   e.preventDefault();
  //   if (image) {
  //     const S =
  //       "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
  //     const N = 16;
  //     const randomChar = Array.from(crypto.getRandomValues(new Uint32Array(N)))
  //       .map((n) => S[n % S.length])
  //       .join("");
  //     const fileName = randomChar + "_" + image.name;
  //     const uploadImg = storage.ref(`images/${fileName}`).put(image);
  //     uploadImg.on(
  //       firebase.storage.TaskEvent.STATE_CHANGED,
  //       () => {},
  //       (err) => {
  //         alert(err.message);
  //       },
  //       async () => {
  //         await storage
  //           .ref("images")
  //           .child(fileName)
  //           .getDownloadURL()
  //           .then(async (url) => {
  //             await db.collection('training_posts').add({
  //               avatar: user.photoUrl,
  //               image: url,
  //               training_name: trainingName,
  //               training_weight: trainingWeight,
  //               training_reps: trainingReps,
  //               timestamp: firebase.firestore.FieldValue.serverTimestamp(),
  //               username: user.displayName,
  //               uid: user.uid
  //             });
  //           });
  //       }
  //     );
  //   } else {
  //     db.collection('training_posts').add({
  //       avatar: user.photoUrl,
  //       image: "",
  //       training_name: trainingName,
  //       training_weight: trainingWeight,
  //       training_reps: trainingReps,
  //       timestamp: firebase.firestore.FieldValue.serverTimestamp(),
  //       username: user.displayName,
  //       uid: user.uid
  //     });
  //   }
  //   setImage(null);
  //   setTrainingName("");
  //   setTrainingWeight("none");
  //   setTrainingReps("0");
  // };
  return (
    <>
      <form>
        <div className={styles.tweet_form}>
          <Avatar
            className={styles.tweet_avatar}
            src={user.photoUrl}
          />
          <input
            className={styles.trainingName}
            placeholder="What kind of training?"
            type="text"
            value={trainingRecord.trainingName}
            onChange={(e) => setTrainingRecord({...trainingRecord, trainingName: e.target.value})}
          />
          <select
            className={styles.trainingWeight}
            value={trainingRecord.trainingWeight}
            onChange={(e) => setTrainingRecord({...trainingRecord, trainingWeight: e.target.value})}
          >
            {weightList.map((weight) => (
              <option key={weight.value} value={weight.value}>
                {weight.label}
              </option>
            ))}
          </select>
          <input
            min="0"
            className={styles.trainingReps}
            placeholder="reps"
            type="number"
            value={trainingRecord.trainingReps}
            onChange={(e) => setTrainingRecord({...trainingRecord, trainingReps: e.target.value})}
          />
          <AddCircleIcon
            className={styles.saveTrainingRecord}
            onClick={() => saveTrainingRecord()}
          />
          <IconButton>
            <label>
              <AddPhotoAlternateIcon
                className={
                  image ? styles.tweet_addIconLoaded : styles.tweet_addIcon
                }
              />
              <input
                className={styles.tweet_hiddenIcon}
                type="file"
                onChange={onChangeImageHandler}
              />
            </label>
          </IconButton>
        </div>
        {/* <Button
          type="submit"
          disabled={!trainingName}
          className={
            trainingName ? styles.tweet_sendBtn : styles.tweet_sendDisableBtn
          }
        >
          Tweet
        </Button> */}
      </form>

    </>
  );
}

export default TweetInput;
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • hoshi-takanori

    2021/02/23 18:47

    let trainingRecords = [{}]; としているので、最初から空オブジェクトが入ってますね。
    また、trainingRecords の内容を書き換える場合、React 的には破壊的変更 (push や shift など) はせずに、useState や Redux を使うべきかと…。

    キャンセル

  • moyong

    2021/02/23 19:16

    ありがとうございます。
    恥ずかしながらuseStateを使うという考えがありませんでした。
    試してみます。

    キャンセル

  • moyong

    2021/02/23 20:04

    配列をもつようなstateをつくり、そこに名前、重さ、回数をもつオブジェクトを追加するような処理を書いてみましたが、型エラー?のようなものがでてしまいました。
    質問を作り直しましたので、アドバイスいただけると幸いですm(__)m
    https://teratail.com/questions/324279

    キャンセル

回答 1

checkベストアンサー

+1

新しい質問には回答がついてるので、こちらに書きます。まず interface trainingRecords ですが、

  • 型の名前の先頭の文字は大文字にしましょう。
  • この型は一つのレコードを表すので、単数形にしましょう。

ということで、型名を TrainingRecord にしましょう。これによって、変数名 trainingRecord, trainingRecords との区別が付きやすくなります。

interface TrainingRecord {
  trainingName: string;
  trainingWeight: string;
  trainingReps: string;
}

このようにキー (trainingName, trainingWeight など) とそれに対する値からなるものを「連想配列」と呼ぶ言語もありますが、JavaScript では単にオブジェクトと呼びます。ので、trainingRecord の型は TrainingRecord 型のオブジェクト、trainingRecords は TrainingRecord 型のオブジェクトの配列になります。


useState の使い方ですが、初期値が [] の場合は型推論が働かないので、useState<型名>(初期値) とすると良いでしょう。なお、TypeScript では TrainingRecord 型のオブジェクトの配列を表す型を TrainingRecord[] と書きます。

const [trainingRecords, setTrainingRecords] = useState<TrainingRecord[]>([]);

次に saveTrainingRecord ですが、trainingRecords は配列なので、要素が一つの場合でも [ 〜 ] で囲んで配列にする必要があります。
また、配列に要素を追加する場合も、やはり [ 〜 ] で囲む必要があります。特に、配列に要素を追加する書き方 [...配列, 要素] はよく出てきますね。(なお、{ 〜 } で囲むとオブジェクトになってしまうのでご注意ください。)

const saveTrainingRecord = () => {
    if (!trainingRecords.length) {
      setTrainingRecords([trainingRecord]);
    } else {
      const newRecord = [...trainingRecords, trainingRecord];
      setTrainingRecords(newRecord);
    }
};

ちなみに、[...trainingRecords, trainingRecord] は trainingRecords が空の場合には trainingRecord ひとつだけの配列になります。ので、次のように書いても同じ動作になります。

const saveTrainingRecord = () => {
    setTrainingRecords([...trainingRecords, trainingRecord]);
};

ところで、TweetInput コンポーネント (TrainingInput の方がいいような気がしないでもない…) では trainingRecords は表示してないようですが、たぶん他のコンポーネントで表示するのでしょうね。その場合、trainingRecords は親コンポーネントで useState してプロパティで渡すか、Redux などを使う方がいい気がします。もっとも、Firebase を使う場合はまた違う (そもそも自分で trainingRecords に追加する必要がない?) かも…。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2021/02/23 22:38 編集

    ありがとうございます!
    今朝からずっとこの問題に取り組んでいまして、ご親切に教えていただいて涙がでそうなほど嬉しいです
    さっそく試してみます。

    キャンセル

  • 2021/02/24 12:44

    おかげさまで、うまくいきました!
    何がオブジェクトで何が配列なのかというところから勉強になりました!
    結果的には、useState<TrainingRecord[]>([]);で型宣言をセットしたことで実現しました
    TweetInputという名前については、udemy教材で作ったアプリを改造している途中でして、改名するのを忘れていました笑ありがとうございます。
    https://gyazo.com/abe888ba6d3cb6e62814a64a8349f57b

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.21%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る