台風も近づき蒸し風呂のような街の中、汗まみれでお過ごしでしょうか。夏です。namio.arakiです。
少し前にリファクタリングでNext.js(バージョン13.4)にフロントエンド側の裏側をリプレイスしたのですが、その際にオブジェクト配列の制御でハマったのでその検証を再度行ってみたいと思います。配列を追加を追加したはずなのに更新されない。コンソールに反映されない。などハマりやすいポイントかと思います。
Next.jsに限らずですが、フレームワークを使うとどうしてもブラックボックスが出てくるので、今回であればJavaScript(TypeScript)空間とNext.js空間での値の持ち方については把握しておいたほうがいいと思います。今回はグローバル空間ではなく、親コンポーネントと子コンポーネントでのやりとりに注目してみます。
素直に $ create-next-app next-research でプロジェクトを作成します。設定は下記としています。
せっかくなので App Router を使用します。
$ create-next-app next-blog √ Would you like to use TypeScript? ... Yes √ Would you like to use ESLint? ... Yes √ Would you like to use Tailwind CSS? ... Yes √ Would you like to use `src/` directory? ... Yes √ Would you like to use App Router? (recommended) ... Yes √ Would you like to customize the default import alias? ... Yes √ What import alias would you like configured? ... @/*
まず最初にコンポーネント内での値の表示についておさらいします。下記のようなオブジェクトを表示したいと思います。
let __objArr = [ { index: 0 }, { index: 1 } ];
以前のNext.jsのバージョンだとオブジェクトをそのままコンポーネントに{__objArr}と記述すると出力で来た気がしますが、最新のバージョンでは下記エラーが出ます。
Unhandled Runtime Error Error: Objects are not valid as a React child (found: object with keys {index}). If you meant to render a collection of children, use an array instead.
なので、map関数で出力してあげます。
<ul className='pl-10'>{__objArr.map((obj)=>{ return ( <li key={obj.index}>インデックス:{obj.index};</li> ) })} </ul>
このままでは、すぐ終わってしまうので、ボタンを設置してこの配列を追加するようにしたいと思います。画面の更新(再レンダリング)が必要になるので、 useState を用いて値を保持するようにします。
const [ objArr, setObjArr ] = useState(__objArr);
ただ App Router を用いている場合、明示的にクライアントコンポーネント(Client Components)とサーバコンポーネント(Server Components)がわかれたので、クライアントコンポーネントを明示しないと下記エラーが出るので、"use client"; をtsxファイルの最初に記述しましょう。
ReactServerComponentsError: You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default. Learn more: https://nextjs.org/docs/getting-started/react-essentials
objArrに対しても__objArrと同様にコンポーネント表示させます。
<ul className='pl-10'>{objArr.map((obj)=>{ return ( <li key={obj.index}>インデックス:{obj.index};</li> ) })} </ul>
またついでにボタンとボタンクリックで叩くメソッドも作成しておきます。
<button className="bg-blue-300 hover:bg-blue-200 text-white rounded px-4 py-2" onClick={addIndex}> 親:ボタン </button>
通常のTypeScript(JavaScript)だけであれば、上記でセットしたaddIndex関数内で配列をpushすればいいと思いますので、一度そのままやってみます。
const addIndex = () => { console.log('>>src/app/page.tsx >addIndex before | __objArr', __objArr) console.log('>>src/app/page.tsx >addIndex before | objArr', objArr) __objArr.push({index:2}); setObjArr(__objArr); console.log('>>src/app/page.tsx >addIndex after | __objArr', __objArr) console.log('>>src/app/page.tsx >addIndex after | objArr', objArr) }
すると、「親:ボタン」をクリックしても画面内に変化は見えません。上記コンソールを見てみると、配列の数は増えています。
>>src/app/page.tsx >addIndex before | __objArr (2)[{...}, {...}] >>src/app/page.tsx >addIndex before | objArr (2)[{...}, {...}] >>src/app/page.tsx >addIndex after | __objArr (3)[{...}, {...}, {...}] >>src/app/page.tsx >addIndex after | objArr (3)[{...}, {...}, {...}]
setObjArrでセットしているものが変わったはずなのに、画面内の表示が変わらない。これはsetObjArrでセットしたものが同じ配列として認識されているためになります。結論からいうと、 オブジェクト配列のstateを更新する場合には必ず新しいオブジェクト配列として認識させる必要があります。
const addIndex = () => { __objArr.push({index:2}); setObjArr( [...objArr, {index:2}]); // 新しいオブジェクトとしてセットする }
上記のようにセットしてみるとどうでしょうか。
すると画面更新はされるのですが、オブジェクトが2つ重複して追加されてしまいます。
新しいオブジェクトとしてセットしたので画面更新はされたのですが、配列は値渡しではなく参照渡しになるためobjArrとobjArrが同じものを参照しているので、同じものに対して「objArr.push」で1つオブジェクトを追加し、さらに「setObjArr」で1つオブジェクトを追加していることになるためです。
そこで、「__objArr.push({index:2});」をコメントアウトして変化を見てみると、画面内は無事オブジェクトが追加されて更新されるのですが、コンソール上では更新されているように見えません。
>>src/app/page.tsx >addIndex after | objArr (2)[{...}, {...}] >>src/app/page.tsx >addIndex after | __objArr (2)[{...}, {...}]
厄介ですね。。逆に__objArrに新しい配列をセットしてそれをsetObjArrしてみます。
const addIndex = () => { __objArr = [...objArr, {index:2}]; // 新しいオブジェクトとしてセットする setObjArr( __objArr); }
結論としてはTypeScript空間での値とNext.js空間での値は一致させておきたいので、__objArrに新しい配列をセットするのが良さそうです。
>>src/app/page.tsx >addIndex after | objArr (2)[{...}, {...}] >>src/app/page.tsx >addIndex after | __objArr (3)[{...}, {...}, {...}]
無事1つだけオブジェクトが追加されましたが、objArrのほうがコンソールではまだ追加されません。これはstateの更新はすぐに実行されず画面更新を待ってから更新されるためなので、スルーして大丈夫なのですが、認識としてすぐに更新されない。setObjArrは即時更新ではない。ということは覚えておかないとハマるポイントになります。
親コンポーネント内で更新がされたので、子コンポーネント内で表示してみたいと思います。
propsでセットします。子(Child)コンポーネントに対して下記のように親コンポーネントでセットしてみます。後述する子コンポーネントからの更新用にaddIndexも渡しておきます。
<Child tsArr={__objArr} stateArr={objArr} addFunc={addIndex} />
src/app/_components/child.tsxを作成して、下記のようにPropsを定義し受け取ります。
type Props = { stateArr: {index:number}[]; tsArr: {index:number}[]; addFunc: () => void; }; const Child: FC<Props> = props => { console.log('>>src/app/_components/child.tsx | props', props); }; export default Child;
コンソールを確認すると下記のように受け取れていることが確認できます。
>>src/app/_components/child.tsx | props {tsArr: Array(2), stateArr: Array(2), addFunc: ƒ}
あとは画面表示用に、childObjArrのステートを作成します。
const [ childObjArr, setChildObjArr ] = useState(props.stateArr);
<div className='text-2xl leading-loose flex'> <div>子)ステート <ul className='pl-10'>{childObjArr.map((obj)=>{ return ( <li key={obj.index}>インデックス:{obj.index};</li> ) })} </ul> </div> </div> <button className="bg-blue-300 hover:bg-blue-200 text-white rounded px-4 py-2" onClick={addChildIndex}> 子:ボタン </button>
ただこのままだとprops.stateArrが変化してもchildObjArrは更新されないため、useEffect で更新をセットします。
useEffect(() => { console.log('>>src/app/_components/child.tsx >useEffect | props.stateArr', props.stateArr); setChildObjArr(props.stateArr); }, [props.stateArr]);
props.stateArrが変化したときにsetChildObjArrが実行されて更新されるので画面も更新されるようになりました。
本来は親コンポーネントで更新したものを子コンポーネントで表示するという一方通行が基本なのですが、後からレイアウト変更などで子コンポーネントから更新をせざるを得ない場合もあるかと思います。その場合、子コンポーネントから親コンポーネントの関数を叩いてみます。上記の通り受け渡しされたprops.addFuncを子コンポーネントから叩きます。
const addChildIndex = () => { props.addFunc(); }
そうすると、子コンポーネントからも親コンポーネントのボタン同様にオブジェクトが追加されたことが画面表示されます。
上記では単純に固定のオブジェクトを追加しているだけですが、クリックごとに新しいindexの値を更新して追加するコードは下記になります。countに関してもTypeScript(JavaScript)とNext.js空間を意識しなければいけないことに注意です。例えば下記で「__count = count + 1」としてますが、「__count++」では値が更新されません。
"use client"; import Image from 'next/image' import {useState} from "react"; import Child from "@/app/_components/child"; export default function Home() { let __objArr = [ { index: 0 }, { index: 1 } ]; const [ objArr, setObjArr ] = useState(__objArr); let __count = 1; const [ count, setCount ] = useState(__count); /*----------------------- メソッド -----------------------*/ const addIndex = () => { // __count++; // ng __count = count + 1; // ok setCount(__count); const newObj = {index:__count}; // __objArr.push(newObj); // ng __objArr = [...objArr, newObj]; // ok 配列を新しく作成する setObjArr(__objArr); } /*----------------------- コンポーネント -----------------------*/ return ( // create-next-appで生成されたデフォのコンポーネントは省略しています <main className="flex min-h-screen flex-col items-center justify-between p-96 pt-24"> <p>src/app/page.tsx</p> <div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]"> <div className='text-2xl leading-loose flex'> <div>1)TypeScript <ul className='pl-10'>{__objArr.map((obj)=>{ return ( <li key={obj.index}>インデックス:{obj.index};</li> ) })} </ul> </div> <hr/> <div>2)ステート <ul className='pl-10'>{objArr.map((obj)=>{ return ( <li key={obj.index}>インデックス:{obj.index};</li> ) })} </ul> </div> </div> </div> <button className="bg-blue-300 hover:bg-blue-200 text-white rounded px-4 py-2" onClick={addIndex}> 親:ボタン </button> <hr/> <Child tsArr={__objArr} stateArr={objArr} addFunc={addIndex} /> </main> ) }
import {FC, useEffect, useState} from "react"; type Props = { stateArr: {index:number}[]; tsArr: {index:number}[]; addFunc: () => void; }; const Child: FC<Props> = props => { let __childObjArr = props.tsArr; const [ childObjArr, setChildObjArr ] = useState(props.stateArr); /*----------------------- useEffect -----------------------*/ useEffect(() => { setChildObjArr(props.stateArr); }, [props.stateArr]); /*----------------------- メソッド -----------------------*/ const addChildIndex = () => { props.addFunc(); } /*----------------------- コンポーネント -----------------------*/ return ( <> <p>src/app/_components/child.tsx</p> <div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]"> <div className='text-2xl leading-loose flex'> <div>子)ステート <ul className='pl-10'>{childObjArr.map((obj)=>{ return ( <li key={obj.index}>インデックス:{obj.index};</li> ) })} </ul> </div> </div> </div> <button className="bg-blue-300 hover:bg-blue-200 text-white rounded px-4 py-2" onClick={addChildIndex}> 子:ボタン </button> </> ); }; export default Child;
いかがだったでしょうか。TypeScript(JavaScript)のネイティブだけで実装している場合に比べ、フレームワークを使うことによりブラックボックスが出てくるので癖を把握するのに手間はかかりますが、条件分岐やタイマーなどでリフレッシュする実装をする必要がなくなるなど便利になる部分は多々あると思います。
MONSTERDIVEではこのようなフレームワークを使う場合やネイティブで書く場合など案件の特性に応じて使い分けていたりします。ネイティブでは書けるけどフレームワークを使ってみたい。またフレームワークは使えるけどネイティブでの理解を深めたい。などチームと一緒に成長できるメンバーを募集しています。少しでも興味があれば、ぜひお気軽にこちらの求人情報ページからお問い合わせください。
一緒にモノづくりをしませんか?