目次
Version 一覧
- React : 18.2.0
- Typescript : 4.9.3
- three.js: 0.146.0
- React-three-fiber : 8.9.1
- react-three-drei : 9.45.0
- web-ifc-three : 0.0.121
- zustand : 4.1.4
- gsap : 3.11.3
- chakra-ui : 2.4.2
React appの立ち上げ
まずは、Reactの新規アプリケーションを立ち上げます。今回はTypescriptも使用していこうと思います。
ターミナルで以下を実行します。
npx create-react-app ifc-viewer --template typescript
作成したアプリのディレクトリに移動してローカルサーバーを立ち上げます。http://localhost:3000 にアクセスし、以下の画面が立ち上がればOKです。
cd ifc-viewer
npm run start dev

不要なファイルを削除します。publicディレクトリとsrcディレクトリの中身を以下を残して全て削除します。

index.tsx / App.tsx / index.htmlの中身を以下のように修正し、表示画面も以下のようになっていればOKです。
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
import "./App.css";
function App() {
return <h1>Hello world</h1>;
}
export default App;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<title>IFC App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

続いてThree.js / React three fiberなど諸々のライブラリをインストールしていきます。バージョンを指定しながらインストールしてますが、最新版が良い方は@0.146などの@以降の記載不要です。ただバージョンによる違いがあるかもしれないのでご注意ください。
npm install three@0.146.0 @react-three/fiber@8.9.1 @types/three@0.146.0 @react-three/drei@9.45.0
とりあえずReact three fiber で球体と表示しOrbitControlsで動かしてみましょう。App.tsxを以下のように書き換えて、画面に表示された球体が動かせればOKです。
import { Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import "./App.css";
function App() {
return (
<Canvas
style={{
width: "100vw",
height: "100vh",
background: "#f0f8ff",
}}
camera={{
fov: 75,
near: 0.1,
far: 200,
position: [10, 4, 10],
}}
>
<OrbitControls makeDefault />
<mesh>
<sphereGeometry />
<meshNormalMaterial wireframe />
</mesh>
</Canvas>
);
}
export default App;

立ち上げはこれで完了です。
IFCモデルを表示
それではIFCモデルを表示していきます。とりあえずweb-ifc-threeをインストールします。
npm install web-ifc-three@0.0.121
もしかしたらエラーが出てきます。web-ifc-three@0.0.121が、threejs@0.146にまだ対応してないのでthreejsのバージョンを落とすか、--forceオプションで半強制的にインストールするように指示があるかと思います。react dreiとの依存関係の兼ね合いもあり、今回は強制的にインストールしていきます。(※本来はしっかりバージョンを合わせるべきかと思いますが・・・)
npm install web-ifc-three@0.0.121 --force
node_modules / web-ifc の中に[ web-ifc-mt.wasm ],[ web-ifc.wasm ] という2つのファイルがあるかと思います。この2つのファイルをpublic直下にコピーしていきます。

ifcデータを読み込む準備をしていきます。src ディレクトリにcomponentsディレクトリを作成し、その中にExperience.tsxを作成、App.tsxに記載したOrbitContorolsをこちらに記載します。また、ambientLightも設置しておきます。
その後、App.tsxでExperience.tsxを読み込みます。同じように球体が表示されていればOKです。
import { OrbitControls } from "@react-three/drei";
const Experience = () => {
return (
<>
<ambientLight intensity={0.5} />
<OrbitControls makeDefault />;
</>
);
};
export default Experience;
import { Canvas } from "@react-three/fiber";
import "./App.css";
import Experience from "./components/Experience";
function App() {
return (
<Canvas
style={{
width: "100vw",
height: "100vh",
background: "#f0f8ff",
}}
camera={{
fov: 75,
near: 0.1,
far: 200,
position: [10, 4, 10],
}}
>
<Experience />
<mesh>
<sphereGeometry />
<meshNormalMaterial wireframe />
</mesh>
</Canvas>
);
}
export default App;
sampleのifcデータをこちらからダウンロードし、publicディレクトリの直下におきます。file名はなんでもいいですが、私は'sample-model.ifc'としてます

こちらのファイルを読み込んでいきます。Experience.tsxを以下のように書き換えます。またApp.tsxに記載の球体は削除します。
import { useThree } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import { IFCLoader } from "web-ifc-three/IFCLoader";
import {
acceleratedRaycast,
computeBoundsTree,
disposeBoundsTree,
} from "three-mesh-bvh";
const Experience = () => {
const { scene } = useThree();
const ifcLoader = new IFCLoader();
ifcLoader.ifcManager.setupThreeMeshBVH(
computeBoundsTree,
disposeBoundsTree,
acceleratedRaycast
);
ifcLoader.ifcManager.setWasmPath("../../");
ifcLoader.load("/sample-model.ifc", (ifcModel) => {
scene.add(ifcModel);
});
console.log(scene);
return (
<>
<ambientLight intensity={0.5} />
<OrbitControls makeDefault />;
</>
);
};
export default Experience;
- 11行目 useThreeからSceneを取り出してきます。
- 13行目 その後ifcLoaderのインスタンスを作成。
- 15行目 ifcLoader.ifcManager.setupThreeMeshBVH …はオブジェクトのピッキングが早くなるようです…正直よく分かってませんがいい感じのライブラリみたいです。こちら参照
- 20行目 その後、wasmファイルを読み込んでます。ここの相対パス若干ハマりましたが…
- 22行目 sampleデータを読み込んで、sceneに追加指定ます。
- 25行目は、一旦sceneの中身を確認しています。
これで以下のように表示されればOKですが、これでは問題があります。developper tool でsceneの中を確認すると、IFC Modelが沢山格納されてしまってます。
これはExperience.tsxが更新されるたびにLoadしてしまっていることが原因です。
useEffectで処理を囲ってあげてもいいのですが、TwitterでR3Fではこうやるんだよと教えていただきました!Pmndrs.Docs

componentsディレクトリ直下にModel.tsxを作成します。こちらでモデルをロードしてprimitiveを返してあげます。また、ifcModelにifcという名前をつけときます。

import { useLoader } from "@react-three/fiber";
import { IFCLoader } from "web-ifc-three";
import { IFCModel } from "web-ifc-three/IFC/components/IFCModel";
import {
acceleratedRaycast,
computeBoundsTree,
disposeBoundsTree,
} from "three-mesh-bvh";
const Model = () => {
const model: IFCModel = useLoader(
IFCLoader,
"/sample-model.ifc",
(loader) => {
loader.ifcManager.setupThreeMeshBVH(
computeBoundsTree,
disposeBoundsTree,
acceleratedRaycast
);
loader.ifcManager.setWasmPath("../../");
}
);
model.name = 'ifc';
return <primitive object={model} />;
};
export default Model;
Experience.tsxで読み込めばOKです。
import { OrbitControls } from "@react-three/drei";
import Model from "./Model";
const Experience = () => {
return (
<>
<Model />
<ambientLight intensity={0.8} />
<OrbitControls makeDefault />
</>
);
};
export default Experience;

これでモデルのロードは完了です。
ロード状態をグローバルStateで管理する
公式ドキュメントにオブジェクトのIDを取得するチュートリアルがあったのでやってみたいのですが、それをやる前にロード状態をグローバルStateで管理しておきます。
グローバルStateの管理には、Redux / Recoil あるいは useContextを使う、 などあるかと思いますが今回はzustandを使用します。
npm install zustand --force
src直下にstoresディレクトリを作成し、その中にuseLoadingState.tsxを作成します。
import { IFCLoader } from "web-ifc-three";
import create from "zustand";
interface LoadedState {
loaded: boolean;
setLoaded: (flg: boolean) => void;
loader: IFCLoader | null;
setLoader: (loader: IFCLoader) => void;
}
export default create<LoadedState>((set) => ({
loaded: false,
setLoaded: (flg: boolean) => {
set(() => {
return { loaded: flg };
});
},
loader: null,
setLoader: (loader: IFCLoader) => {
set(() => {
return { loader: loader };
});
},
}));
loadedをfalse => trueにすることで、ロード完了としています。
また、後々別のとこでIFCLoaderを使用するので、IFC Loaderもグローバルに管理します。
それではロード完了後 loaded 変数をtrueに、setLoaderでloaderを更新していきます。
R3F公式Docsより、useLoaderの第4引数にcallback関数を取ることで、ロードの進捗が取得できますが、今回はifcManagerが用意してくれているsetOnProgressを使用します。
Model.tsxを修正していきます。
import { useLoader } from "@react-three/fiber";
import { IFCLoader } from "web-ifc-three";
import { IFCModel } from "web-ifc-three/IFC/components/IFCModel";
import {
acceleratedRaycast,
computeBoundsTree,
disposeBoundsTree,
} from "three-mesh-bvh";
import useLoadingState from "../stores/useLoadingState";
import { ParserProgress } from "web-ifc-three/IFC/components/IFCParser";
const Model = () => {
const { setLoader, setLoaded } = useLoadingState((state) => state);
const model: IFCModel = useLoader(
IFCLoader,
"/sample-model.ifc",
(loader) => {
loader.ifcManager.setupThreeMeshBVH(
computeBoundsTree,
disposeBoundsTree,
acceleratedRaycast
);
loader.ifcManager.setWasmPath("../../");
loader.ifcManager.setOnProgress((event: ParserProgress) => {
const ratio = event.loaded / event.total;
ratio === 1 && setLoaded(true);
});
setLoader(loader);
}
);
model.name = "ifc";
return <primitive object={model} />;
};
export default Model;
- これでLoadingの状態をグローバルに管理することができました。
オブジェクトのID取得
公式ドキュメントのオブジェクトIDの取得を実装してみます。hooks直下にuseIdPicker.tsxを作成します。
import { useFrame, useThree } from "@react-three/fiber";
import { IFCModel } from "web-ifc-three/IFC/components/IFCModel";
import { useEffect, useRef, useState } from "react";
import { Object3D } from "three";
import useLoadingState from "../stores/useLoadingState";
import useFocusId from "../stores/useFocusId";
const useIdPicker = () => {
const { scene, raycaster, gl } = useThree();
const canvas = gl.domElement;
const { loaded, loader } = useLoadingState((state) => state);
const idRef = useRef<string>("");
const [rayObjects, setRayObjects] = useState<Object3D[] | null>(null);
useEffect(() => {
if (loaded) {
const model = scene.children.filter((mesh) => {
const ifc = mesh.name === "ifc" && mesh;
return ifc;
});
setRayObjects(model);
canvas.addEventListener("dblclick", () => {
console.log(idRef.current);
});
}
}, [loaded]);
useFrame(() => {
if (rayObjects && rayObjects.length > 0) {
raycaster.firstHitOnly = true;
const obj = raycaster.intersectObjects(rayObjects);
if (obj.length > 0 && loader && loaded) {
const ifcObject = obj[0];
const index = ifcObject.faceIndex;
const ifcModel = ifcObject.object as IFCModel;
const geometry = ifcModel.geometry;
const ifc = loader.ifcManager;
const id: string = index
? ifc.getExpressId(geometry, index).toString()
: "";
idRef.current = id;
} else {
idRef.current = "";
}
}
});
return;
};
export default useIdPicker;
- まずはグローバルに管理しているloaderとloadedrをインポートします。
- useThreeからscene ,raycaster, glを取得し、glからcanvas要素を取得します。
- useRefで取得したidを更新します。
- useStateではidを取得する対象のモデル(IFCModel)を管理します。初期値はnullでロードが完了したら事前にモデルにつけておいたifcという名前のMeshを探し、セットします。
- useEffect内でロードが完了後、IFCModelを取得し、raycastの対象オブジェクトとしてセットしています。またcanvasをダブルクリックしたら、現在取得しているidをconsoleで出力するようにしています。
- useFrame内でraycastで最初にぶつかったオブジェクトを取得し、そのIDを取得してuseRefの値を更新しています。IFCModelとぶつかっていない場合は、useRefの値を''に更新しています。
Experience.tsx内で実行してあげます。
import { OrbitControls } from "@react-three/drei";
import useIdPicker from "../hooks/useIdPicker";
import Model from "./Model";
const Experience = () => {
useIdPicker();
return (
<>
<Model />
<ambientLight intensity={0.8} />
<OrbitControls makeDefault />
</>
);
};
export default Experience;
- 以下の画像のように、consoleで確認すると、IDが取得できてそうです。

取得したIDを画面に表示
画面上にIDを表示していきます。CSSのフレームワークChakraUIを使用していきます。まずはインストールします。
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion --force
取得したIDをグローバルStateで管理していきます。storesディレクトリ直下にuseFocusId.tsxを作成します。その後、useIdPicker.tsx内で取得したIDを更新します。
import create from "zustand";
interface FocusId {
focusId: string;
setFocusId: (id: string) => void;
}
export default create<FocusId>((set) => ({
focusId: "",
setFocusId: (id: string) => {
set(() => {
return { focusId: id };
});
},
}));
import { useFrame, useThree } from "@react-three/fiber";
import { IFCModel } from "web-ifc-three/IFC/components/IFCModel";
import { useEffect, useRef, useState } from "react";
import { Object3D } from "three";
import useLoadingState from "../stores/useLoadingState";
import useFocusId from "../stores/useFocusId";
const useIdPicker = () => {
const { scene, raycaster, gl } = useThree();
const canvas = gl.domElement;
const { loaded, loader } = useLoadingState((state) => state);
const { setFocusId } = useFocusId((state) => state);
const idRef = useRef<string>("");
const [rayObjects, setRayObjects] = useState<Object3D[] | null>(null);
useEffect(() => {
if (loaded) {
const model = scene.children.filter((mesh) => {
const ifc = mesh.name === "ifc" && mesh;
return ifc;
});
setRayObjects(model);
canvas.addEventListener("dblclick", () => {
setFocusId(idRef.current);
});
}
}, [loaded]);
useFrame(() => {
if (rayObjects && rayObjects.length > 0) {
raycaster.firstHitOnly = true;
const obj = raycaster.intersectObjects(rayObjects);
if (obj.length > 0 && loader && loaded) {
const ifcObject = obj[0];
const index = ifcObject.faceIndex;
const ifcModel = ifcObject.object as IFCModel;
const geometry = ifcModel.geometry;
const ifc = loader.ifcManager;
const id: string = index
? ifc.getExpressId(geometry, index).toString()
: "";
idRef.current = id;
} else {
idRef.current = "";
}
}
});
return;
};
export default useIdPicker;
componentsディレクトリにInterface.tsxを作成します。その後App.tsxでInterface.tsxをインポートします。
ID が空文字でない時、画面左上にIDが表示されればOKです。
import { Box } from "@chakra-ui/react";
import useFocusId from "../stores/useFocusId";
const Interface = () => {
const { focusId } = useFocusId((state) => state);
return (
<>{focusId !== "" && <Box sx={idDisplayStyle}>{`ID : ${focusId}`}</Box>}</>
);
};
export default Interface;
const idDisplayStyle = {
width: "90px",
height: "40px",
display: "flex",
top: "0",
left: "0",
marginLeft: "5px",
lineHeight: "40px",
padding: "0 5px 0 5px",
background: "rgba(255, 255, 255, 0.8)",
textAlign: "center",
position: "absolute",
};
import { Canvas } from "@react-three/fiber";
import "./App.css";
import Experience from "./components/Experience";
import Interface from "./components/Interface";
function App() {
return (
<>
<Canvas
style={{
width: "100vw",
height: "100vh",
background: "#f0f8ff",
}}
camera={{
fov: 75,
near: 0.1,
far: 200,
position: [10, 4, 10],
}}
>
<Experience />
</Canvas>
<Interface />
</>
);
}
export default App;

ロード画面を作成

最後に上のGIF画像のようなロード画面を作っていきます。fadeoutする処理にライブラリgsapを使用するので、インストールします。
npm install gsap --force
とりあえずInterface.tsxにLoadingバーを書いていきます。
import { Box } from "@chakra-ui/react";
import useFocusId from "../stores/useFocusId";
const Interface = () => {
const { focusId } = useFocusId((state) => state);
return (
<>
{focusId !== "" && <Box sx={idDisplayStyle}>{`ID : ${focusId}`}</Box>}
<Box id="barContainer" sx={barContainerStyle}>
<Box id="loadingBar" sx={loadingBarStyle} />
<Box id="loadingText" sx={loadingTextStyle}>
Loading...
</Box>
</Box>
</>
);
};
export default Interface;
const idDisplayStyle = {
width: "90px",
height: "40px",
display: "flex",
top: "0",
left: "0",
marginLeft: "5px",
lineHeight: "40px",
padding: "0 5px 0 5px",
background: "rgba(255, 255, 255, 0.8)",
textAlign: "center",
position: "absolute",
};
const barContainerStyle = {
height: "100vh",
width: "100vw",
top: "0",
alignItems: "center",
position: "absolute",
};
const loadingBarStyle = {
top: "50%",
width: "100vw",
height: "2px",
position: "absolute",
background: "black",
transform: "scaleX(1)",
transformOrigin: "top center",
transition: "transform 1.0s",
};
const loadingTextStyle = {
width: "100%",
fontSize: "20px",
textAlign: "center",
position: "absolute",
top: "45%",
};

ロードの進捗によって、このバーとLoadingのテキストを変更していきます。
まずは、Loading中にカメラの前に配置する白いPlane Meshを生成していきたいのですが、その前にPlaneMeshのStateを管理するためにstoresディレクトリにuseOverrayState.tsxを作成します。
import create from "zustand";
interface OverrayState {
removeOverray: boolean;
setRemoveOverray: (flg: boolean) => void;
}
export default create<OverrayState>((set) => ({
removeOverray: false,
setRemoveOverray: (flg: boolean) => {
set(() => {
return { removeOverray: flg };
});
},
}));
次にplaneMeshを作成していきます。componentsディレクトリにLoadingOverRay.tsxを作成し、Experience.tsxで読み込みます。
plameMeshはShaderを使用してfadeoutさせていきます。また、removeOverrayがtrueになったらsceneから削除するようにしています。
import * as THREE from "three";
import useOverrayState from "../stores/useOverrayState";
const overlayGeometry = new THREE.PlaneGeometry(300, 300, 1, 1);
const overlayMaterial = new THREE.ShaderMaterial({
transparent: true,
uniforms: {
uAlpha: { value: 1.0 },
uColor1: { value: new THREE.Color("#f0f8ff") },
uColor2: { value: new THREE.Color("#ffffff") },
},
vertexShader: `
varying vec2 vUv;
void main()
{
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
vUv = uv;
}
`,
fragmentShader: `
varying vec2 vUv;
uniform float uAlpha;
uniform vec3 uColor1;
uniform vec3 uColor2;
void main()
{
float strength = distance(vUv, vec2(0.5));
vec3 color = mix(uColor1, uColor2, strength + 0.2 );
gl_FragColor = vec4(color, uAlpha);
}
`,
});
const LoadingOverRay = () => {
const { removeOverray } = useOverrayState((state) => state);
return !removeOverray ? (
<mesh
geometry={overlayGeometry}
material={overlayMaterial}
position={[0, 0, 14]}
rotation-y={Math.PI * 0.25}
name="overray"
dispose={null}
/>
) : null;
};
export default LoadingOverRay;
import { OrbitControls } from "@react-three/drei";
import useIdPicker from "../hooks/useIdPicker";
import LoadingOverRay from "./LoadingOverRay";
import Model from "./Model";
const Experience = () => {
useIdPicker();
return (
<>
<LoadingOverRay />
<Model />
<ambientLight intensity={0.8} />
<OrbitControls makeDefault />
</>
);
};
export default Experience;
次にInterface.tsxのloadingBarStyleのtransform: "scaleX(1)"を0にしときます。
.....省略
const loadingBarStyle = {
top: "50%",
width: "100vw",
height: "2px",
position: "absolute",
background: "black",
transform: "scaleX(0)",
transformOrigin: "top center",
transition: "transform 1.0s",
};
.....省略
最後にModel.tsxにローディング中の処理を追記します。(※すみません、ここコードかなり汚いです。hooksに切り出そうとしたのですが、ifcManagerのsetonprogressの扱いが難しく、試行錯誤してこんな感じになっちゃいました......)
import { useLoader, useThree } from "@react-three/fiber";
import { IFCLoader } from "web-ifc-three";
import { IFCModel } from "web-ifc-three/IFC/components/IFCModel";
import {
acceleratedRaycast,
computeBoundsTree,
disposeBoundsTree,
} from "three-mesh-bvh";
import useLoadingState from "../stores/useLoadingState";
import { ParserProgress } from "web-ifc-three/IFC/components/IFCParser";
import useOverrayState from "../stores/useOverrayState";
import { gsap } from "gsap";
const Model = () => {
const { gl, scene } = useThree();
const canvas = gl.domElement;
const loadingBar = document.getElementById("loadingBar");
const loadingText = document.getElementById("loadingText");
const barContainer = document.getElementById("barContainer");
const { setLoader, setLoaded } = useLoadingState((state) => state);
const { setRemoveOverray } = useOverrayState((state) => state);
const handleLoading = () => {
setLoaded(true);
if (loadingText && barContainer) {
loadingText.innerHTML = "Go to Model !!";
loadingText.style.cursor = "pointer";
loadingText.addEventListener("click", () => {
barContainer.style.display = "none";
canvas.style.background =
"linear-gradient(0deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 35%, rgba(0,212,255,1) 100%)";
const overrayMesh = scene.children.filter((mesh) => {
if (mesh.name === "overray") {
return mesh as THREE.Mesh;
}
});
const mesh = overrayMesh[0] as unknown as THREE.Mesh;
const material = mesh.material as unknown as THREE.ShaderMaterial;
gsap.to(material.uniforms.uAlpha, { duration: 1, value: 0 });
setTimeout(() => {
setRemoveOverray(true);
}, 500);
});
}
};
const model: IFCModel = useLoader(
IFCLoader,
"/sample-model.ifc",
(loader) => {
loader.ifcManager.setupThreeMeshBVH(
computeBoundsTree,
disposeBoundsTree,
acceleratedRaycast
);
loader.ifcManager.setWasmPath("../../");
loader.ifcManager.setOnProgress((event: ParserProgress) => {
const ratio = event.loaded / event.total;
loadingBar!.style.transform = `scaleX(${ratio})`;
ratio === 1 && handleLoading();
});
setLoader(loader);
}
);
model.name = "ifc";
return <primitive object={model} />;
};
export default Model;
- setOnProgressでローディングの進捗が取得できます。ratioが<1未満だとロード中、1になったらロード完了です。
- ratio < 1の時は、loadingBarをratioでx方向にscaleさせることで、ロードの進捗に伴ってバーが伸びていくようにしています。
- ratio === 1になったら、handleLoading関数を実行します。
- handleLoading関数では、setLoadedでロード状態のStateを更新。
- Loadingのスタイルを変更し、テキストをクリックしたら、テキストやバーをdisplay:noneにして、Plane Meshをfadeoutさせてます。その0.5s後にplaneMeshを削除しています。
完成
以上になります。普段からIFCデータやIFC.jsを触っているわけではないので、変な記述などあったら教えていただけると嬉しいです。m(_ _)m
ライブラリのインポートを --forceで強制的にインポートしているのがちょっと・・・って感じですが、この辺はしっかりバージョン合わせていく必要はあるかなと思います。
ソースコードはgithubにあげてますのでご興味あれば。
[ 参考 ]
- IFC.js Docs : https://ifcjs.github.io/info/ja/docs/Introduction
- Svelte × Typescript × IFC.jsでIFCをWeb上に表示 : https://zenn.dev/masamiki/articles/c9a34119acfd6c
- pmndrs Docs : https://docs.pmnd.rs/
以上
【2022年最新】React(v18)完全入門ガイド|Hooks、Next.js、Redux、TypeScript HTML、CSS、JavaScriptの基礎を終えた方に最適!React入門の決定版!Reactについて知っておくべき基礎知識について体系的、かつ網羅的に学習して、最短でReactをマスターしよう!