0
0

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 1 year has passed since last update.

Vue.js ちょっとかじった俺的 React 勉強ログ

Last updated at Posted at 2023-12-21

動機

以前の投稿見た人はわかるかもだけど、Vue.jsでそれなりに満足はしていたし今でもしているのだが、世の中いろんなところで ReactReact 言われててなんとなく「今どき React わかるのは最低要件っス」的な圧を感じるので、一通りやっておかんとなと思ってはじめた。最近だとUnityからも使えるらしい。まじかよ。

で、Vue.jsやってた人がこれはReactだとどうなるの?的な観点からざざっとやってみた

勉強ログ

Step.0

ただここのチュートリアルやってるといつの間にか(Next.js/Remix/Gatsby)を使ったパターンに落とし込まれるのが罠。求めていたのは Vue.js の snap-in-replace としての React だったのでこのトレーニングはスキップした。

Step.1 : Hello Worldを手元で

この手順は公式では既に deprecated 扱いになっている `npx create-react-app` は既にdeprecatedの扱い。

参考: https://zenn.dev/nekoya/articles/dd0f0e8a2fa35f

ここではフレームワークに依存しない学習のためにあえてやっている。使わなくてもスケルトンを git clone などして自分で用意して、npm install && npm start すれば同じことになるので大丈夫。

npx create-react-app するのに Node.js 以外に特に事前に何か入れておく必要はない。

npx create-react-app step1
cd step1
npm start

ディレクトリの構造には特に決まったルールはないようだが、慣例的に以下のようにしている例が多い。

./
├── build/ # <- npm run build で作成される配布用ファイル
├── node_modules/
├── package-lock.json
├── package.json
├── public/ # <- 画像ファイルなどのリソース
└── src/
    ├── index.js # <- 最初に呼び出されるjs
    ├── pages/ # <- ページを定義するコード
    ├── setupTests.js
    ├── lib/ # <- 外部APIなどを呼び出すためのコード
    └── ui/  # <- UIのパーツを定義するコード

内容を変更するには src/App.js を変更する。試しに以下のように変更してみる

src/App.js
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      Hello React
    </div>
  );
}

export default App;

変更を保存すると、ホットロードされて表示が更新される。

View Code on CodeSandbox:

Step.2 : 複数のページ。 React Router

公式ページ: https://reactrouter.com/en/main

npx create-react-app step2
cd step2
npm install react-router-dom
npm start

Step.2-1

src/index.js を以下のように変更する

src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: <div>This is root page</div>,
  },
]);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

保存するとブラウザには This is root page と出ているはず。

Step.2-2

複数ページを作る。まずRootページ

src/pages/Root.js
const Root = () => {
    return (
        <div id="sidebar">
            This is Root page
        </div>
    );
};

export default Root;
src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";
import Root from './pages/Root';

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
  },
]);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

This is Root page と出る

Mainページを作る

src/pages/Main.js
const Main = () => {
    return (
        <div>
            This is Main page
        </div>
    )
}

export default Main

index.js にルートを追加する

src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";
import Root from './pages/Root';
import Main from './pages/Main';

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
  },
  {
    path: "/main",
    element: <Main />
  }
]);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

ブラウザで http://localhost:3000http://localhost:3000/main で表示が変わることを確認する

Step.3 : コンポーネントのライフサイクル

- Vuejs.BeforeMount == React.funcBeforeReturn

React では特定のディレクティブというものはなくて、普通の Javascript で処理する。return する前の処理はすべて BeforeMount の処理ということになる。

こんな感じ

src/pages/Root.js
const Root = () => {
    // return 前の処理はすべて BeforeMount の処理ということになる
    const mysecretstring = "aaabbbccc"
    return (
        <div id="sidebar">
            This is Root page.<br />
            {/* {} で囲った部分はjavascriptが実行される */}
            My Secret string is {mysecretstring}.
        </div>
    );
};

export default Root;

- Vuejs.(Mounted|Unmounted) == useEffect

例えばサーバのログアウトAPIを呼び出すなど、画面から移動したときになにかしたいとき、ReactではuseEffectを使う。

import { useEffect } from 'react';
import { initConnection,
         connect,
         disconnect } from './chat.js';

export default function ChatRoom() {
  const beforeMount = () => {
    /* マウント前の処理 */
    initConnection();
  }
  useEffect(() => {
    /* マウント後の処理 */
    connect();
    return () => {
        /* アンマウント後の処理。クリーンアップ関数とも呼ばれる */
        disconnect();
    }, []);
  return <h1>Welcome to the chat!</h1>;
}

Step.4 変数のリアクティビティ == useState

  • ローカル変数はレンダー間で保持されない
  • ローカル変数の変更は、レンダーをトリガしない

で、useState フックを使ってリアクティブな変数を宣言する。

src/pages/Root.js
import { useState } from "react";
import NumberHolder from "../ui/NumberHolder";
const Root = () => {
    // var number = 1;
    const [number, setNumber] = useState(0); // 0 は初期値
    const decrement = () => {
        //number -= 1
        setNumber(number-1)
        console.log(number)
        return(number)
    };
    const increment = () => {
        //number += 1
        setNumber(number+1)
        console.log(number)
        return(number)
    };
    return (
        <div id="sidebar">
            <button onClick={decrement}>-</button>
            <NumberHolder number={number}/>
            <button onClick={increment}>+</button>
        </div>
    );
};

export default Root;
src/ui/NumberHolder.js
const NumberHolder = ({number}) => {
    console.log("number", number)
    //const number = obj.number
    return (
        <input name="aaa" value={number} onChange={e => console.log(e)}/>
    )
}
export default NumberHolder;

View Code on CodeSandbox:

Step.5 v-for, v-if

Vue.js で言うところの v-for は、Reactではレンダリング時にJSXから呼び出したJavascriptで行うため、専用の句は存在しない。例えば以下のように書く。

src/pages/ListExample.js
import ListContainer from "../ui/ListContainer";
const ListExample = () => {
  const items = ["aaa", "bbb", "ccc"];
  return (
    <div id="list">
      This is list demo(aka. v-for of vue.js)
      <br />
      <a href="/if">v-ifのデモはこちら</a>
      <br />
      <ListContainer items={items} maxChars={100} />
    </div>
  );
};
export default ListExample;
src/ui/ListContainer.js
import ListItem from "./ListItem";
const ListContainer = ({items}) => {
    console.log("number of items:", items.length)
    const listItems = items.map(item => {
        return (
            <ListItem listKey={item} listValue={item}/>
        )
    })
    return (
        <ul>
            {listItems}
        </ul>
    )
}
export default ListContainer;
src/ui/ListItem.js
const ListItem = ({listKey, listValue}) => {
    console.log(listKey, listValue)
    return (
        <li key={listKey}>{listValue}</li>
    )
}
export default ListItem;

同様に v-if もJSXのJavascriptで制御する

src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
  createBrowserRouter,
  RouterProvider
} from "react-router-dom";
import ListExample from "./pages/ListExample";
import IfExample from "./pages/IfExample.js";

const router = createBrowserRouter([
  {
    path: "/",
    element: <ListExample/>,
  },
  {
    path: "/if",
    element: <IfExample/>,
  }
])

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RouterProvider router={router}/>
  </React.StrictMode>
);
src/pages/IfExample.js
import { useState } from "react";
import NumberHolder from "../ui/NumberHolder";
import ListContainer from "../ui/ListContainer";
const IfExample = () => {
    const items = [
        "aaa",
        "aaabbb",
        "aaabbbccc"
    ]
    const [number, setNumber] = useState(10);
    const decrement = () => {
        //number -= 1
        setNumber(number-1)
        console.log(number)
        return(number)
    };
    const increment = () => {
        //number += 1
        setNumber(number+1)
        console.log(number)
        return(number)
    };
    return (
        <div id="list">
            <button onClick={decrement}>-</button>
            <NumberHolder number={number}/>
            <button onClick={increment}>+</button>
            文字以上の項目を非表示にする
            <ListContainer items={items} maxChars={number}/>
        </div>
    )
}
export default IfExample;
src/ui/ListContainer.js
import ListItem from "./ListItem";
const ListContainer = ({items, maxChars}) => {
    console.log("number of items:", items.length, "max characters", maxChars)
    const listItems = items.map(item => {
        return (
            <ListItem listKey={item} listValue={item} maxChars={maxChars}/>
        )
    })
    return (
        <ul>
            {listItems}
        </ul>
    )
}
export default ListContainer;
src/ui/ListItem.js
const FilteredItem = ({listKey, listValue, maxChars}) => {
    if (listValue.length < maxChars) {
        console.log("smaller", listValue, maxChars)
        return (
            <li key={listKey}>{listValue}</li>
        )
    } else {
        console.log("bigger", listValue, maxChars)
        return null
    }
}
const ListItem = ({listKey, listValue, maxChars}) => {
    console.log(listKey, listValue, maxChars)
    return (
        <FilteredItem listKey={listKey} listValue={listValue} maxChars={maxChars}/>
    )
}
export default ListItem;

上で使われている <FilteredItem listKey={listKey} listValue={listValue} maxChars={maxChars}/>listKey={listKey} とか listValue={listValue} をReact用語でprop(s)と呼ぶ。

じゃあ変化する変数は最上位コンポーネントに持たせて下位にはpropsで渡せばいいじゃんと考えるかもしれない(私も最初はそうした)が、変化したpropsを受け取った下位コンポーネントは再レンダリングが走るというのが曲者。うっかり最上位に頻繁に変更されるような変数をもってpropsで派生させていくと、なにかするたびに画面が再レンダリングの嵐になってまともに使えなくなる。

再レンダリングを防ぐ方法はいろんな人が書いてるので別途参照

Step.6 UIコンポーネントを使う

などなど・・・

Step.7 パッケージング、ホスティング

Vue.js と同じように npm run build するといろいろ難読化されパッケージングされて、./build 以下に静的ファイルが作成される。あとはこれを NGINX などでホスティングすればいい。

Step.8 やってはいけないこと

useEffectの中でstateの参照と更新をやってはいけない

例えば以下のようなコードがあったとする

app.js
const [myState, setMyState] = useState(0)

useEffect(() => {
   DoSomething()
   .then((ret) => setMyState(ret))
   .then(() => {
      if(myState>10)
         console.log("myState is larger than 10")
      else
         console.log("myState is up to 10")
   })
},[myState])

実際に動かすと、myStateが更新されるたびにuseEffectの中が再実行されるので、useEffect内が無限ループする。

これを避けるには、別のところにクロージャとして定義して、それをいじるようにすればいい。

func.js
export const MyState = (() => {
   let mystate = 0
   const set = (val) => {
      mystate = val
   }
   const get = () => {
      return mystate
   }
   return {
      set: (val) => set(val),
      get: () => get()
   }
})()
app.js
import { MyState } from './func.js'

const myState = MyState

useEffect(() => {
   DoSomething()
   .then((ret) => {
      myState.set(ret)
   }).then(() => {
      if(myState.get()>10)
         console.log("myState is larger than 10")
      else
         console.log("myState is up to 10")
   })
}, [myState])

やっておいたほうがいいこと

Reactのコードを読んだり、なにかトラブルを解決するために、以下のことを理解しておくとなんとかなることがままある。参考- Reactの前提条件(https://kinsta.com/jp/blog/react-best-practices/#react-1)

上記のサイトだと前提条件って書かれているが、自分の感覚だとそこまでstrictなものじゃないけどサンプルコードとかにこういう書き方が多いので知っておくと何をやっているか理解できたり、前述したprops,useEffectまわりでいい感じに動かすために知っておくと便利、という感じ。

Reactは結果的にわりとモダンな書き方が求められることがままあるので、Vue.jsと比較すると相対的に難易度が高いようには感じる。逆に言うとVue.jsはそこまで知らなくてもなんとなくそれなりのアプリが作れてしまうという敷居の低さ的なものはあるんじゃないかな、と。お気持ちですけど・・・。

さいごに

特にありません

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?