以前、バックエンドエンジニアのためのVue.js、React、Angular入門という記事を作成し、その後にSvelteが登場、またReactの全盛に合わせ、それに影響を受けたJSX活用のフレームワークも多数登場しました。中でもSolidJS(以下、Solidと表記)の普及率が上昇してきたので、第5弾として作ってみました。
ちなみにSolidJSですが、Ryan Carniatoというサンノゼ在住の人物が主となって開発、後は多国籍の複数人チームによって改良が続けられています。また、記法はReact寄りですが、書きやすさ、親しみやすさなどはVueやSvelteを強く意識しているので、SvelteのコントロールフローやVueのライフサイクルフックといった痒いところに手が届くシステムを実装しています(自分がReactより気に入った点)。なので、フロントエンドツールとしても重宝しそうです(ただし、useReducerのような状態管理フックが未熟で、公式でもReduxを改造して使用しているなど改良余地はまだまだあります)。
Next開発企業であるVercelやJetBrains、Netlifyなどがメインスポンサーに入っている点でも今後に注目でしょう。
※有識者から指摘を受けながら自分用の備忘録としても作っているところです。
solidの動作環境を作成
solidの操作環境作成は至ってシンプルです。また、Viteを標準実装しているので、起動も至って迅速(1秒ぐらい?)です。
#npx degit solidjs/template 任意のプロジェクト
#cd 任意のプロジェクト
#npm i
これでnpmをインストール(依存関係の対応)してから、ローカルサーバに接続させるためvite.config.ts
から以下のように追記します。
server: {
host: '0.0.0.0', //追記
port: 3000,
},
これで、Webサーバを再起動してから
#npm run dev
これで起動することができます。Solidのプロジェクトは以下の構造となっています(ファイル名がtsxになっていますが、jsxとして利用できます)。
■任意のプロジェクト名
-■ node_modules
-■ src
- App.module.css
- App.tsx
- index.tsx
- package.json
主なフォルダとファイルを抜粋してみました。そして、App.tsxは以下のようになっています。ほとんどReactと同じですが、それよりもっとシンプルです。
const app =()=>{
//ここに処理や仕様フックなどを記述
return(
//ここにJSXを記述
);
}
export default app;
演習1:フォーム操作:テキスト文字を表示させる
バインディングの基礎の動きで、入力された文字を表示させる仕組みです。これをSolidで記述してみます。ここで、早速注意点があります。
import type { Component } from 'solid-js';
import {createSignal } from 'solid-js';
const App: Component = () => {
const [mes,setMes] = createSignal(null)
const changeMes= (e)=>{
setMes(e.target.value);
}
return (
<div>
<input onInput={changeMes} />
<p>入力された文字<span>{mes() }</span></p>
</div>
);
};
export default App;
Reactと異なる点として、同期(リアクティブ)対象の変数を作成するにはuseStateの代わりに、createSignalというフックを利用します(詳しくは第3章で)。
import { createSignal } from 'solid-js';
const [hoge,setHoge] = createSignal("");
それからイベント用のメソッドですが、SolidにはonChange
とonInput
があり、リアルタイムで表示させる場合、後者を利用します(onChangeの場合、フォームからフォーカスが外れたタイミングで処理が実行されます)。
※この場合は静的なリテラル処理なのでmesとしても展開できますが、動作保証をしていないとのことなので、createSignalはhoge()で展開するものだと覚えた方がいいでしょう(修正指摘事項1)。
演習2:プルダウンメニュー(ループ処理)
では今度はループ処理を用いてプルダウンメニューを作成してみます。Solidはmapで作成していくこともできるのですが、Forという便利なループ処理用の部品(コントロールフロー)が用意されています。
import {For } from 'solid-js'; //ここからForを呼び出す
const App =()=>{
//変数定義
const [sel,setMenu] = createSignal("");
const ary_data = [
{id: 1,name: "cakePHP"},
{id: 2,name: "Laravel"},
{id: 3,name: "Code Igniter"},
{id: 4,name: "Symfony"},
{id: 5,name: "Zend Framework"},
{id: 6,name: "Yii"},
];
//イベント処理
const changeMenu = (e)=>{
setMenu(e.target.value ); //フォームから取得した値
}
//レンダリング
return(
<>
<select onChange={changeMenu}>
<option>選択</option>
<For each={ary_data}>
{(data,idx)=>(
<option value={data.id}>{data.name}</option>>
)}
</For>
</select>
<p>選択された値:<span>{sel}</span></p>
</>
);
}
export default App;
For
solidにはVueのようなForというコントロールフローがあり、こちらで記述したほうがすっきりします(特に検索システムのように初期値が空白の状態でループしなければいけない場合、重宝します)。またReactのmapと違って、For内では複数のタグを記述してもエラーにならず、keyによる一意指定も不要な上、returnも不要です。
※似た並列処理式にIndexというのもあり、こちらはプリミティブな変数に対して用いるものだそうです。
Forの式は以下のようになっています(Indexも同じ記述)。
<For each={値を展開させたいオブジェクト}>
{(item,idx)=>(
//JSXオブジェクト
)}
</for>
上記の例の場合、ary_dataがループ対象となります。
<For each={ary_data}>
{(data,idx)=>(
<option value={data.id}>{data.name}</option>>
)}
</For>
演習3:検索フォーム(値の連動)
では、検索フォームの連動をSolidで実践してみます。
import type { Component } from 'solid-js';
import {createSignal,createEffect,For } from 'solid-js';
const App =()=>{
//変数定義
const [word$,setWord] = createSignal(""); //検索文字の制御
const [searched$ ,setSearched] = createSignal([]); //検索結果の制御
const ary_data = [
{id: 1,name: "モスクワ",img:"Moscow.jpg"},
{id: 2,name: "サンクトペテルブルク",img:"Sankt.jpg"},
{id: 3,name: "エカテリンブルク",img:"Yekaterin.jpg"},
{id: 4,name: "ムンバイ",img:"Mumbai.jpg"},
{id: 5,name: "ベンガルール",img:"Bengaluru.jpg"},
{id: 6,name: "コルカタ",img:"Kolkata.jpg"},
{id: 7,name: "サンパウロ",img:"SaoPaulo.jpg"},
{id: 8,name: "リオデジャネイロ",img:"Rio.jpg"},
{id: 9,name: "ブラジリア",img:"Brasilia.jpg"},
];
//createEffectフックによって、検索値がバインドされたときに処理が実行される
createEffect( ()=>{
let searched = []
if(word$() != ""){
searched = ary_data.filter((item,idx)=>{
return item.name.search(word$())!== -1;
});
}
setSearched(searched);
});
//検索文字のバインド
const bindWord = (e)=>{
let word = e.target.value
word = word.replace(/[\u3041-\u3096]/g, function(match) {
let chr = match.charCodeAt(0) + 0x60;
return String.fromCharCode(chr);
})
setWord(word); //検索文字のバインド
}
//検索文字のクリア
const clear = ()=>{
let word = ''
setWord(word);
}
//レンダリング
return(
<>
<input type="text" onInput={bindWord} value={word$()} />
<p>検索結果{searched$().length}件</p>
<button onClick={ clear }>文字のクリア</button>
<ul>
<For each={searched$()}>
{(data,index)=>(
<li>
<dl>
<dt>{ data?.name }</dt>
<dd >image</dd>
</dl>
</li>
)}
</For>
</ul>
</>
);
}
export default App;
createSignal
ここでcreateSignalについて詳しく説明します。第1章にも登場しましたが、具体的にはデータを監視するためのフックです。
ReactのuseStateとよく似ているのですが、
const [hoge,setHoge] = createSignal[null]
この戻り値hogeはプリミティブな変数ではなく、コールバック関数となっています。なので、リテラルやフォームのプロパティとして値を展開する場合は{ hoge() }としないと展開できません。当記事では、最初のうちはわかりやすく明示するためにこの章ではコールバック関数に対しhoge$と、約物を付与しています。
したがって、今回の場合、For文でeachに設定する値もsearched$()となります。
createEffect
ReactのuseEffectに似たエフェクトフック(React公式でも、ようやく副作用という誤訳をやめたようです)で、連動処理を実行したい場合に用いると効果的です。
ただ、useEffectといくつか違いがあります。まず、第二引数が存在せず、creteSignalで監視された変数すべてが連動対象となります(第7章で後述するonでそれを限定することもできます)。便利な反面、無限ループにも陥りやすいので利用には注意が必要です。
また、Solidには後述するライフサイクルフック、onMountがあるので初期設定したい場合のみcreateEffectを利用するという手段は無用です(useEffectの挙動変更に伴う二重制御の苦痛からも開放されます)。
演習4: データリストの追加、削除、修正(DOMの再生成)
では、CRUDの基本となるデータリストの追加、削除、修正についても記述したいと思います。
import type { Component } from 'solid-js';
import {createSignal,onMount, Index } from 'solid-js';
const App =()=>{
//変数定義
const [item$, setItem] = createSignal('');
const [bind$ ,setBind] = createSignal([]); //フックの使用(データリストの更新)
const ary_data = [
{id: 1,name: "モスクワ"},
{id: 2,name: "サンクトペテルブルク"},
{id: 3,name: "エカテリンブルク"},
{id: 4,name: "ムンバイ"},
{id: 5,name: "ベンガルール"},
{id: 6,name: "コルカタ"},
{id: 7,name: "サンパウロ"},
{id: 8,name: "リオデジャネイロ"},
{id: 9,name: "ブラジリア"},
];
//新規作成用のメソッド
const ins = ()=>{
setBind([
...bind$(),
{
id: bind$().length + 1,
name: item$()
}
]) //新規作成
}
//削除用のメソッド
const del = (idx)=>{
setBind(bind$().filter((_, i) => i !== idx)) //削除処理
}
//修正用のメソッド
const upd = (idx,name)=>{
changeValue(idx,name)
}
const changeValue = (idx,item)=>{
let edit = []
bind$().map((elem,i)=>{
if(i !== idx){
edit = [...edit,elem]
}else{
edit = [...edit,{id:i + 1, name: item}]
}
})
setBind(edit)
}
//onMountを用いると、一回だけ読み込む処理ができる
onMount( ()=>{
setBind(ary_data) //空オブジェクトに代入
});
//レンダリング
return(
<>
<label>新規<input type="text" onInput={ (e)=>{ setItem(e.target.value) } } value={item$()}/></label>
<button type="button" onClick={ ins }>新規</button>
<ul>
<Index each={bind$()}>
{(data,idx)=>(
<li>
<dl>
<dt><input type="text" onChange={ (e)=>{ changeValue(idx,e.target.value) } } value={ data().name } /></dt>
<dd><button type="button" onClick={ ()=> upd(idx,data().name) }>修正</button></dd>
<dd ><button type="button" onClick={ ()=> del(idx) }>削除</button></dd>
</dl>
</li>
)}
</Index>
</ul>
</>
);
}
export default App;
ライフサイクルフック(onMount)
solidJSにはVueのようなライフサイクルフックも用意されています(種類はまだまだ少ないですが)。onMountはJSXレンダリング後に1回のみ処理を行うフックなので、初期値を設定したい場合に重宝します(第7章のように、jsonデータをインポートしたい場合などにも)。
データの登録
データを登録する場合はonClickイベントでメソッドを実行し、あとは分割代入を用いてデータ挿入を行うだけです。
データの削除
データを削除する場合は、対象のインデックスを取得し、対象の値を削除したものをsetBindで更新するだけです。
データの修正
データを修正する場合、修正対象をテキストフォームに表示させる時にはonChangeの方が適切です(更新を行うのはフォーカスが外れる時だけでいいので)。そして修正値を分割代入して更新しています(コンポーネントを用いないと変化が分かりづらいですが、直前の値を削除した時に、処理結果がわかると思います)。
createStore
今度はcreateStoreというモジュールを使用して再作成しています。オブジェクト、とりわけ複数のプロパティを持った変数を制御する場合に適しており、createStoreを通すことでストアオブジェクト内の全プロパティに対し、逐一createSignalと同じリアクティブ制御してくれます。
設定方法はcreateSignalと同じで、以下の式となります(solid-jsからではありません)。
import { cteateStore } from 'solid-js/store'; //インポート先に注意
const [hoge,setHoge] = createStore([]);
ただし、createStoreでストアオブジェクトを作成し状態管理した場合は再代入禁止です。何も施さない場合は登録と更新で書き換えが必要ですが、それを解決してくれる便利なメソッドがあります。
produce
ストアオブジェクトに再代入を可能にするのがproduceというメソッドで、本章のプログラムでは登録と修正に活用しています。ただし、このproduceはsetter(creteStoreで作成したsetHoge)内でしか用いることができませんので注意が必要です。
登録のように、push関数を使えたり、または修正のようにオブジェクトを再代入できたりできます。また、後になればメソッドそのものを代入したりもできます。
ストアオブジェクトで作り変えたのが以下のプログラムになります。
import type { Component } from 'solid-js';
import {createSignal,onMount, Index } from 'solid-js';
import {createStore,produce } from 'solid-js/store';
const App =()=>{
//変数定義
const [item, setItem] = createSignal('');
const [upditem, setupdItem] = createSignal('');
const [bind ,setBind] = createStore([]); //ストアの使用(データリストの更新)
const ary_data = [
{id: 1,name: "モスクワ"},
{id: 2,name: "サンクトペテルブルク"},
{id: 3,name: "エカテリンブルク"},
{id: 4,name: "ムンバイ"},
{id: 5,name: "ベンガルール"},
{id: 6,name: "コルカタ"},
{id: 7,name: "サンパウロ"},
{id: 8,name: "リオデジャネイロ"},
{id: 9,name: "ブラジリア"},
];
//新規作成用のメソッド
const ins = ()=>{
setBind(
produce((bind)=>
bind.push({
id:bind.length + 1,
name:item(),
})
)
);
};
//削除用のメソッド
const del = (idx)=>{
setBind(bind.filter((_, i) => i !== idx)) //削除処理
}
//修正用のメソッド
const upd = (idx,id,name)=>{
setBind(
produce((bind)=>{
bind[idx] = {
id :id,
name :name,
}
})
)
}
//onMountを用いると、一回だけ読み込む処理ができる
onMount( ()=>{
setBind(ary_data) //空オブジェクトに代入
});
//レンダリング
return(
<>
<label>新規<input type="text" onChange={ (e)=>{ setItem(e.target.value) } } value={item()}/></label>
<button type="button" onClick={ ins }>新規</button>
<ul>
<Index each={bind}>
{(data,idx)=>(
<>
<li>
<dl>
<dt><input type="text" onInput={ (e)=>{ setupdItem(e.target.value) } } value={ data().name } /></dt>
<dd><button type="button" onClick={ ()=> upd(idx,data.id,upditem())}>修正</button></dd>
<dd ><button type="button" onClick={ ()=> del(idx) }>削除</button></dd>
</dl>
</li>
</>
)}
</Index>
</ul>
</>
);
}
export default App;
演習5:コンポーネント制御(電卓)
ここから複数のコンポーネントを用いたシステム制御を演習していきます。
●コンポーネントの基本構造
Solidのコンポーネント呼び出しはReactとほぼ同じといっていいです。
●親子のコンポーネントを同一ファイルに記述する場合の注意
Reactは親子関係にあるコンポーネントを同一ファイルで処理することも多いですが、関数コンポーネントで親子のコンポーネントを制御する場合、は外部に出力する必要がある親コンポーネントだけexport default Parentとしてください。子コンポーネントまで下手にexport default Childとしてしまうと、制御エラーとなります。
Const Child()=>{
//子コンポーネントは外部に出力しないので、そのまま記述
}
Const Parent()=>{
//親コンポーネントは外部に出力するので、後で関数だけを記述する
}
export deafault Parent //親コンポーネントを出力する
●親コンポーネントから子コンポーネントへの値の受け渡し
子コンポーネントに値を受け渡すには定義関数の引数にprops
と記述するだけで、子コンポーネントでpropsオブジェクトから値を取得できるようになります。なおpropsとはpropatiesの略で、属性、所有のことです。つまりは親コンポーネントが所有している変数という意味合いで捉えれば、子コンポーネントはその親コンポーネントが持っている変数を参照しているだけだとわかるでしょう。
●Solidでの親コンポーネントの記述
では、親子コンポーネントの働きを解説するため、具体的に電卓を作っていきます。なお、ファイル名は以下のようになっています。
- 親コンポーネント Calc.js
- 子コンポーネント Setkey.js //プッシュキー
●親コンポーネントの制御
親コンポーネントは以下のようになっています。
import { createSignal } from "solid-js";
import Setkey from "./Setkey";
const Calc = ()=>{
import { createSignal } from "solid-js";
import Setkey from "./Setkey";
const Calc = ()=>{
const [str,setStr] = createSignal("");
const [sum,setSum] = createSignal(0);
const pushkeys = [
[['7','7'],['8','8'],['9','9'],['div','÷']],
[['4','4'],['5','5'],['6','6'],['mul','×']],
[['1','1'],['2','2'],['3','3'],['sub','-']],
[['0','0'],['eq','='],['c','C'],['add','+']],
];
//子コンポーネントからの値の受取
const onReceive = (typed,sum)=>{
setStr(str() + typed);
setSum(sum);
}
return (
<>
<Setkey pushkeys={pushkeys} onEmit={(c,s)=>onReceive(c,s)} />
<div>
<p>打ち込んだ文字:{str}</p>
<p>合計:{sum}</p>
</div>
</>
)
}
export default Calc;
●子コンポーネントの制御
子コンポーネントは以下のようになっています。
import { For,onMount } from "solid-js";
import { createStore } from "solid-js/store";
const Setkey = (props)=>{
const[ state, setState ] = createStore([]); //文字列
const pushkeys = props.pushkeys; //親コンポーネントからの受け渡し
const onEmit = props.onEmit; //親コンポーネントからの受け渡し
const fstate = {
lnum: null, //被序数
cnum: 0, //序数
sign: "",
};
//ステータスの設定
onMount(()=>{
setState(fstate);
})
//プッシュキーのイベントメソッド
const getChar = (chr,str)=>{
let lnum = state.lnum; //被序数
let cnum = state.cnum; //序数
let sign = state.sign; //計算記号
let sum = 0 //合計
if(chr.match(/[0-9]/g)!== null){
let num = parseInt(chr)
cnum = cnum * 10 + num //数値が打ち込まれるごとに桁をずらしていく
}else if(chr.match(/(c|eq)/g) == null){
if(lnum != null){
lnum = calc(sign,lnum,cnum)
}else{
if(chr == "sub"){
lnum = 0
}
lnum = cnum
}
sign = chr
cnum = 0
}else if( chr == "eq"){
lnum = calc(sign,lnum,cnum)
sum = lnum
}else{
lnum = null
cnum = 0
sum = 0
}
setState({lnum,cnum,sign}) //変数の同期用
onEmit(str,sum); //親コンポーネントへの送出
}
//計算処理
const calc = (mode,lnum,cnum)=>{
switch(mode){
case "add": lnum = cnum + lnum
break;
case "sub": lnum = lnum - cnum
break;
case "mul": lnum = lnum * cnum
break;
case "div": lnum = lnum / cnum
break;
}
return lnum
}
return (
<>
<For each={pushkeys}>
{(val,key)=>(
<div>
<For each={val}>
{(v,i)=>(
<button class="square" onClick={()=>{getChar(v[0],v[1])}}>
{v[1]}
</button>
)}
</For>
</div>
)}
</For>
</>
)
}
export default Setkey;
●親コンポーネントから子コンポーネントへの値の受け渡し
親コンポーネントから子コンポーネントへ値を受け渡すには以下のように記述し、右側のオブジェクトリテラルに転送したい変数を記述し、左側のコールバック関数に格納します。
<子コンポーネント コールバック関数={子コンポーネントに転送したい変数} />
具体的には親コンポーネントにおける以下の部分になります。
<Setkey pushkeys={pushkeys} onEmit={(c,s)=>onReceive(c,s)} />
●親コンポーネントの変数を子コンポーネントで受け取る
この変数は子コンポーネントで以下のように受け取ります。propsプロパティをコンポーネントの引数に代入し、それを以下のように展開します。Reactとは異なり、展開した値はリアクティビティを維持しているので、そのまま展開できます(指摘修正点2)。
const Setkey = (props)=>{
const pushkeys = props.pushkeys; //親コンポーネントからの受け渡し
const onEmit = props.onEmit; //親コンポーネントからの受け渡し
今回の場合はリアクティビティを維持する必要がないので、以下の方法でも一応は展開可能です(VueでonRef系を通さない形、つまりリアクティブの対象から外れる)。
const {pushkeys,onEmit} = props;
●子コンポーネントから親コンポーネントにデータを受け渡す
子コンポーネントから親コンポーネントにデータを受け渡す場合は、子コンポーネントのメソッドの引数を用意しておき、親コンポーネントに用意されたコールバック関数を実行する際に、インライン関数の引数から値を取り出すことができます。
具体的には子コンポーネントに記述した送出用変数onEmit(名前は任意)に代入した変数str、sumを親コンポーネントで受け取ることになります。
onEmit(str,sum); //親コンポーネントへの送出
対する親コンポーネントではコールバック関数から受け取ることができます。
//子コンポーネントからの値の受取
const onReceive = (typed,sum)=>{
setStr(str() + typed);
setSum(sum);
}
<Setkey pushkeys={pushkeys} onEmit={(c,s)=>onReceive(c,s)} />
このコンポーネントに記述している変数c,sが子コンポーネント上のメソッドonEmitと結びついているので、そのまま、受取用の変数onReceiveで受け取ることができます。
onEmit={(c,s)=>onReceive(c,s)}
オブジェクトの展開はコールバック関数ではなくて、ストアオブジェクトの変数となります。したがって上記の電卓の場合、stateオブジェクトから展開します。ただし、監視対象となったストアオブジェクトはproxy制御がかかるのでcreateSignalと異なり値の再代入は一切できません(エラー表示されます)。その代わり、setHogeに値を代入すればいいので、createSignalで管理するより利便性は高いです。その場合、監視対象となっているプロパティのみ更新可能です(紐づかないプロパティは一切同期できません)。
※配列の全部が全部createStoreで管理するのも得策ではなく、第3章にあった検索結果などのようにプロパティに対する再代入がなく、また配列の件数が頻繁に上下するオブジェクトはcreateSignalの方が高パフォーマンスを維持できます(つまりリアクティビティの必要性の有無に左右される)。
演習6:ルーティング(買い物かご)
ではSolidでもルーティング機能を用いたSPAに入っていきます。
●ルーティング制御する
Solidでルーティング制御をする場合、@solidjs/routerというライブラリが必須となります。事前にインストールしておきましょう。
# npm i @solidjs/router
●ルーティングの仕組み
ルーティングの仕組みは、それぞれのリンクタグに対し、リンクの表示先をRouteタグで表示するというものです。なので、表示先をシェアする関係にあるので、Routerというタグで表示を切換制御しています。
※実はSolidjs/routerが明瞭で優秀だったので、後追いでReact-router-dom6からSolidjs-routerと同じ仕様に変えているようです。
データ構造
■component
- ■css(デザイン制御用)
- ■pages
- Products.tsx //商品一覧(ここに商品を入れるボタンがある)
- Cart.tsx //買い物かご(ここに商品差し戻し、購入のボタンがある)
- ShopContext.tsx //オブジェクト情報の格納
- GlobalState.tsx //親コンポーネント(ルーティングもここに記述)
- Reducer.tsx //共通の処理制御用
●ルーティング制御の記述
以下がルーティングの基本例となります。注意点は、Router内にRouteタグを記述する必要があります。また、ナビゲーション部分ですが、実はaタグでも対応しており、aタグの場合、Routerタグの外側に設置することができます(Navigateという部品もありますが、これを使用した場合はRouterの内側のみ有効で、動的にリンク先を遷移させたい場合に用いるようです)。
どのJSフレームワークよりもシンプルに記述できるので、ナビとルーティングはトップコンポーネント(GlobalState.tsx)に実装しています。
import { Route, Router } from '@solidjs/router';
import Cart from './pages/Cart';
import Products from './pages/Products';
return (
<>
<ShopContext.Provider value={{store:store,dispatch:dispatch}}>
<div class="tab-a-base">
<header class="main-navigation">
<nav>
<ul>
<li><a href="/Cart">Cart({cnt().cart})</a></li>
<li><a href="/Products">Products({cnt().articles})</a></li>
</ul>
</nav>
</header>
</div>
<Router>
<Route path="/Cart" component={Cart} />
<Route path="/Products" component={Products} />
<Route path="/Articles" component={Articles} />
<Route path="/Details/:id" component={Details} />
</Router>
</ShopContext.Provider>
</>
);
};
export default Global;
●ルーティングの記述
pathが遷移先、componentが遷移先のコンポーネントの使用オブジェクトになります。
<Route path="/Cart" component={Cart} />
●リンク先
リンク先はhrefプロパティが遷移先となります。
<a href="/Cart">Cart</a>
●データをやりとりする
Solidでデータをやりとりする場合、データ情報をストレージとして状態管理しておくと便利なので、前述のcreateStoreが大活躍します。また、この変数を各コンポーネントに分配するときに役立つの今回の主役となるcreateContextとuseContextです。よく似ていますが、働きは逆で、createContextはcontext作成、対するuseContextはcontextの受取に用い、データオブジェクトをcontext化しておくとやりとりが楽です。
createContext
createContextはcontextオブジェクトを作成するフックです。なので、今回はシステムで制御するためのデータファイルに対し、context化しておきます。
import {createContext } from 'solid-js';
const Values = {
products:[
{ id: "p1", title: "花王 バブ ゆず", price: 60 ,stock: 10 },
{ id: "p2", title: "バスクリン きき湯", price: 798 , stock: 3 },
{ id: "p3", title: "アース 温素 琥珀の湯", price: 980, stock: 2 },
{ id: "p4", title: "白元アース いい湯旅立ちボトル", price: 398, stock: 6 },
{ id: "p5", title: "クラシエ 旅の宿", price: 598, stock: 7 }
],
cart: [],
articles: [],
money: 10000,
total: 0, //残額
};
export const ShopContext = createContext(Values); //context化しておく
useContext
対するuseContextはデータの受取に用います。このcontextにストアオブジェクトを代入することもできます。
また、SolidもReact同様に、
<コンテクスト名.Provider value={転送したいオブジェクト} >
//任意のコンポーネント
</コンテクスト名.Provider>
とすることで、任意のコンテクストをどのコンポーネントにも送出することができるので、ここに先程のルーティング先のすべてをラッピングします。
※まだ説明していない記述もありますが、下半分がルーティング制御部分です。
import { useContext,createSignal,createMemo } from 'solid-js';
import { createStore,produce } from 'solid-js/store';
import { Route, Router } from '@solidjs/router';
import Cart from './pages/Cart';
import Products from './pages/Products';
import Articles from './pages/Articles';
import Details from './pages/Details';
import { ShopContext } from './pages/ShopContext';
import {shopReducer } from './Reducer';
const GlobalState = () => {
const [cnt,setCnt] = createStore({cart:0,articles:0});
const ctx = useContext(ShopContext);
const [store,setStore] = createStore(ctx);
const dispatch = (mode,data)=>{
setStore(produce((store)=> shopReducer(mode,data,store)));
}
const itemsCounter = (items)=>{
return items.reduce((count,artItem)=>{
return count + artItem.quantity //購入の個数
},0);
}
createMemo(()=>{
setCnt(
produce((cnt)=>{
cnt.cart = itemsCounter(store.cart);
cnt.articles = itemsCounter(store.articles);
})
)
})
return (
<>
<ShopContext.Provider value={{store:store,dispatch:dispatch}}>
<div class="tab-a-base">
<header class="main-navigation">
<nav>
<ul>
<li><a href="/Cart">Cart({cnt.cart})</a></li>
<li><a href="/Products">Products({cnt.articles})</a></li>
</ul>
</nav>
</header>
</div>
<Router>
<Route path="/Cart" component={Cart} />
<Route path="/Products" component={Products} />
<Route path="/Articles" component={Articles} />
<Route path="/Details" component={Details} />
</Router>
</ShopContext.Provider>
</>
);
};
export default GlobalState;
●各コンポーネントの記述
ルーティング先の各コンポーネントの記述はこのようになっています。このルーティング先は全部、ShopContextというcontextによって紐付けられているので、ここからJSX及びデータの値をオブジェクトとして取得しています。
また、同系統コンテクストからイベントとデータを同期するには
onClick= {()=>context.methodHoge(payload)}
とすれば大丈夫で(payloadは任意のデータオブジェクト)、親コンポーネントにデータを返すことも簡単にできます。
受取先
データの受取は前述したようにuseContextを使用します。また、Proxy制御がかかっているので、Vue、Angular、Svelteなどのパスワードトークンの代わりに、同一のcontextオブジェクトを利用します(したがって、contextファイルのインポートが必要です)。
カート画面
カート画面は以下のようになっています。ここでShowという初登場のコントロールフローが登場します。
import { For,Show,useContext } from "solid-js";
import { ShopContext } from './ShopContext';
import '../Cart.css';
const Cart = ()=>{
const ctx = useContext(ShopContext); //受け取ったcontextオブジェクト
return(
<>
<main class="cart" >
<Show when={ctx.store.cart?.length <= 0}>
<p>No Item in the Cart!</p>
</Show>
<ul>
<For each={ctx.store.cart}>
{(cartitem,i)=>(
<li>
<div>
<strong>{ cartitem.title }</strong> - { cartitem.price }円
({ cartitem.quantity })
</div>
<div>
<button onClick={()=>ctx.dispatch("remove",cartitem.id)}>買い物かごから戻す(1個ずつ)</button>
</div>
</li>
)}
</For>
</ul>
<h3>合計: {ctx.store.total}円</h3>
<h3>残高: {ctx.store.money}円</h3>
<Show when={ctx.store.money - ctx.store.total >= 0} >
<button onClick={()=>ctx.dispatch("buy",null)}>購入</button>
</Show>
</main>
</>
)
}
export default Cart;
●Show
Showは、JSXを表示させるかどうかを判定することができます(ifのようなもの)。そのときのプロパティはwhenとなります。ちなみに条件分岐ができるMatchというものもあります。いずれも複数行のJSX、複数タグにも対応しているので、非常に便利です。
<If when={条件文}>
//複数行のJSXでも問題ない
</If>
商品一覧
商品一覧はこのようになっています。
import { For,useContext } from "solid-js";
import { Link } from '@solidjs/router';
import { ShopContext } from './ShopContext';
import '../Cart.css';
const ProductsPage = props =>{
const ctx = useContext(ShopContext);
return(
<>
<main className="products">
<ul>
<For each={ctx.store.products}>
{(product,idx)=>(
<>
<li>
<div>
<strong><a href={`./Details/${product.id}`}>{product.title}</a></strong> - {product.price}円 【残り{product.stock}個】
</div>
<div>
{product.stock > 0 && <button onClick={()=>ctx.dispatch("add",product)}>かごに入れる</button>}
</div>
</li>
</>
)}
</For>
</ul>
</main>
</>
)
}
export default ProductsPage
処理ファイル
処理用の関数は一旦、定義関数dispatch(任意の名称です)で継承しておいて、そこにuseReducerのような引数(モード,差分)を用意します。なお、ストアオブジェクトはトップコンポーネント(GlobalState)で管理しているので引数としては不要です。先ほど、このアクション継承用のdispatchもcontextで受け渡しておいたのはそのためで、これでどのコンポーネントからも処理用のメソッドを一元化
できます。
const addProductToCart = (product, state) => {
let cartIndex = null
const stat = state
//買い物かごの調整
const updatedCart = [...stat.cart];
const updatedItemIndex = updatedCart.findIndex(
item => item.id === product.id
);
if (updatedItemIndex < 0) {
updatedCart.push({ ...product, quantity: 1,stock: 0 });
cartIndex = updatedCart.length -1 //カートの最後尾
} else {
const updatedItem = { ...updatedCart[updatedItemIndex] }
updatedItem.quantity++;
updatedCart[updatedItemIndex] = updatedItem;
cartIndex = updatedItemIndex //加算対象のインデックス
}
//商品在庫の調整
const updatedProducts = [...stat.products] //商品情報
const productid = updatedCart[cartIndex].id //在庫減算対象の商品
const productIndex = updatedProducts.findIndex(
p => productid === p.id
)
const tmpProduct = { ...updatedProducts[productIndex] }
tmpProduct.stock-- //在庫の減算
updatedProducts[productIndex] = tmpProduct
//合計金額の調整
const total = stat.total
const sum = getSummary(updatedCart,total)
stat.cart = updatedCart
stat.products = updatedProducts
stat.total = sum
};
//カートから商品の返却
const removeProductFromCart = (productId, state) => {
const updatedCart = [...state.cart];
const updatedItemIndex = updatedCart.findIndex(item => item.id === productId);
const updatedItem = { ...updatedCart[updatedItemIndex] }
updatedItem.quantity--
if (updatedItem.quantity <= 0) {
updatedCart.splice(updatedItemIndex, 1);
} else {
updatedCart[updatedItemIndex] = updatedItem;
}
//商品在庫の調整
const updatedProducts = [...state.products] //商品情報
const productIndex = updatedProducts.findIndex(
p => p.id === productId
)
const tmpProduct = { ...updatedProducts[productIndex] }
tmpProduct.stock++ //在庫の加算
updatedProducts[productIndex] = tmpProduct
let summary = getSummary(updatedCart,state.total)
state.total = summary;
state.cart = updatedCart;
state.products = updatedProducts;
};
//購入手続
const buyIt = (state)=>{
let cart = [...state.cart];
let money = state.money
let total = state.total
let articles = state.articles; //所持品
for( let val of cart){
const articlesIndex = articles.findIndex(
a => a.id === cart.id
)
if (articlesIndex < 0) {
articles = [...state.cart];
} else {
const tmpArticles = { ...articles[articlesIndex] }
tmpArticles.quantity++;
articles[articlesIndex] = tmpArticles;
}
}
let summary = getSummary(cart,total)
let rest = money - summary
cart.splice(0)
summary = 0
state.total = summary;
state.money = rest;
state.cart = cart;
state.articles = articles;
}
const getSummary = (cart,total)=>{
const sum = cart.reduce((total,{price = 0,quantity})=> total + price * quantity,0)
return sum
}
export const shopReducer = (mode, data,store) => {
switch (mode) {
case "add":
return addProductToCart(data,store);
case "remove":
return removeProductFromCart(data,store);
case "buy":
return buyIt(store);
default:
return store;
}
};
この各種処理によるデータの差分を回収し、ストアオブジェクトの同期処理を実行しているのが以下の箇所になります。メソッド内で代入処理する方法でも同期が取れるみたいです(修正指摘点)。
const dispatch = (mode,data)=>{
setStore(produce((store)=> shopReducer(mode,data,store)));
}
Svelteのstoreオブジェクトに近い動きをしていますが、こちらの方が、ずっと記述が楽だと感じます。
コールバック制御
SolidにはuseCallbackに相当する関数は現時点で存在しません。ですがcreateMemoで働きを代用できるようです。
import { useContext,,createMemo } from 'solid-js';
const GlobalState = () => {
//カウンター用の関数
const itemsCounter = (items)=>{
return items.reduce((count,artItem)=>{
return count + artItem.quantity //購入の個数
},0);
}
createMemo(()=>{
setCnt(
produce((cnt)=>{
cnt.cart = itemsCounter(store.cart);
cnt.articles = itemsCounter(store.articles);
})
)
})
/*中略*/
}
●詳細画面を作成する(パラメータのやりとり)
詳細画面
詳細画面には以下のようになっています。Reactのようカスタムコンポーネントを活用することもできます。
import { useContext } from "solid-js";
import { ShopContext } from './ShopContext';
import { Route, Router,useParams,useSearchParams, useNavigate } from '@solidjs/router';
const Details = ()=>{
const ctx = useContext(ShopContext);
const { id } = useParams() //useParamsはパスをそのまま抽出できる
//const [query,setQuery] = useSearchParams(); //useSearchParamsはクエリをオブジェクトとして取得できる
const navigate = useNavigate();
//カスタムコンポーネント作成し、そこで準備する
const ShowItem = ()=>{
const selid = `${query.id}` //取得したパラメータを検索用idに修正
const item = ctx.store.products.find((item)=> item.id == selid) //一致するアイテム取得
return(
<li>{item.title}</li>
)
}
return(
<>
<ul>
<ShowItem />
</ul>
<button onClick={()=>navigate('/Products')}>戻る</button>
</>
)
}
export default Details;
Solidでも詳細画面を作成する場合、パラメータのやりとりが必須になってきますが、React同様、pathのやりとりがリテラルないなので、プロパティに変数や関数をダイレクトに埋め込むことができます。
<a path={`/detail/${product.id}`}>{product.title}</a>
また、トップコンポーネントのRouterタグ内は以下のように追記しておきます。React同様に:idがパラメータ名となります。
<Router>
<Route path="/Cart" component={Cart} />
<Route path="/Products" component={Products} />
<Route path="/Articles" component={Articles} />
<Route path="/Details/:id" component={Details} />
</Router>
●パスパラメータを取得する
パスパラメータを取得する場合はuseParamsを活用できます。そして、このuseParamsはオブジェクトをそのまま抽出できるので便利です。
Detail.js
import React,{useState,useContext} from "react";
import ShopContext from "../context/shop-context";
import { useParams } from '@solidjs/router'; //クエリ取得用
const { id } = useParams() //useParamsはパスをそのまま抽出できる
●クエリを使ってデータをやりとりする
URLに埋め込まれたクエリパラメータに対して、同じ操作をしてみます。クエリの場合でもuseSearchParamsを使えば簡単にオブジェクトを取得できます。同期用の戻り値もあります。
Detail.js
import { useSearchParams } from '@solidjs/router';
export Details = ()=>{
const [query,setQuery] = useSearchParams(); //クエリ取得用
const getCallback = useCallback(()=>{
const selid = `${query.id}` //取得したパラメータを検索用idに修正
const item = context.products.find((item)=> item.id == selid)
setItem(item)
},[query])
}
●戻るボタンを実装する
ページ遷移はuseNavigateを使用します。このメソッドは引数に遷移先のパスを明示的に記述でき、後にreact-router-dom6以降にも採用されました。
import {useNavigate } from '@solidjs/router';
/*中略*/
const navigate = useNavigate()
navigate('/') //ホームに戻る
演習7:スタイル制御(写真検索システム)
Solidでもスタイル制御の演習として、いいねボタンを実装してみます。
SolidでFont Awesomeを活用する場合は、solid-faをnpmなどからインストールしておいて下さい。また、font-asesomeのアイコンライブラリのインストールも事前に必要です。
import type { Component } from 'solid-js';
import {createSignal,createEffect,createMemo,onMount,For,on } from 'solid-js';
import {createStore,produce,reconcile} from "solid-js/store";
import lodash from 'lodash';
import './Tests.css';
import Fa from 'solid-fa';
import { faHeart } from '@fortawesome/free-solid-svg-icons' //ライブラリ
const Tests =()=>{
//変数定義
const [word, setWord] = createSignal(''); //文字検索
const [country ,setCountry] = createSignal([]); //国の選択
const [areas ,setAreas] = createSignal(); //州の選択
const [cities ,setCities] = createStore([]); //都市の一覧
const [hit_areas,setHitAreas] = createSignal([]);
const [hit_cities,setHitCities] = createStore([]);
const [hit_cities_by_word,setHitCitiesByWord] = createStore([]);
const [hit_cities_by_area,setHitCitiesByArea] = createStore([]);
const [hit_word,setHitWord] = createSignal([]);
const [image,setImage] = createSignal('');
const [target, setTarget] = createSignal(''); //スタイル変化の対象
const act = '';
const active = 'red';
onMount( async ()=> {
await fetch('./json/city.json').then(res=>res.json()).then(res => setCities(res))
await fetch('./json/state.json').then(res=>res.json()).then(data => setAreas(data))
})
const countries = [
{ab:"US",name:"United States"},
{ab:"JP",name:"Japan"},
{ab:"CN",name:"China"},
];
//検索文字のバインド
const bindWord = (e)=>{
let word = e.target.value
word = word.replace(/[\u3041-\u3096]/g, function(match) {
let chr = match.charCodeAt(0) + 0x60;
return String.fromCharCode(chr);
})
searchWord(word); //検索文字のバインド
}
//検索文字のクリア
const clear = ()=>{
setCountry([]);
setAreas();
setWord(null);
setHitAreas(reconcile([]));
setHitCities(reconcile([]));
setHitWord(reconcile([]));
}
//国から州を絞り込み
const selCountry = (e)=>{
const sel_country = e.target.value;
const hit_area = areas().filter((item,key)=>{
return item.country === sel_country
})
setHitAreas(hit_area)
setCountry(sel_country);
}
//エリアから該当する都市を絞り込み
const selArea = (e)=>{
const hits = cities.filter((item,key)=>{
return item.state === e.target.value
})
setHitCitiesByArea(hits);
}
//文字検索一致
const searchWord = (e)=>{
const hits = cities.filter((elem,idx)=>{
return elem.name.toLowerCase().indexOf(e) !== -1
})
setHitCitiesByWord(hits)
setHitWord(e);
}
//連動制御
createMemo(()=>{
const len_areas = hit_cities_by_area.length;
const len_word = hit_cities_by_word.length;
let hits = []
if(len_areas > 0 && len_word > 0 ){
hits = lodash.intersection(hit_cities_by_area, hit_cities_by_word)
}else if(len_areas > 0){
hits = hit_cities_by_area;
}else if(len_word > 0){
hits = hit_cities_by_word;
}else{
hits = [];
}
if(hits.length > 0){
setHitCities(hits)
}else{
setHitCities(reconcile([]));
}
})
//アイコンの色変化をリストに反映
createEffect(on(target,(target)=>{
const selidx = cities.findIndex((v)=>{ return v.id === target.id });
setCities(produce((cities)=> cities[selidx] = target))
}))
//画像の調整
const imageSet = (area,name)=>{
if(name !== undefined){
let imgname = name.toLowerCase();
const imgfile= imgname.replace(/\s+/g,"_");
const imgsrc = `./img/${area}_${imgfile}.jpg`;
return imgsrc;
}
}
//スタイル制御
const colorSet = (id)=>{
let selidx = hit_cities.findIndex((v)=>{ return v.id === id })
const item = hit_cities[selidx]
let color = "";
if(item.act === active ){
color = "black";
}else{
color = "red";
}
setHitCities(
produce((hit_cities)=>{
hit_cities[selidx]= { ...item,...{act: color}};
})
);
setTarget(hit_cities[selidx]);
}
return(
<>
<label> 国の選択 </label>
<select id="sel1" onChange={selCountry }>
<option value="">-- 国名を選択 --</option>
<For each={countries}>{(country,idx)=>(
<option value={country.ab}>{country.name}</option>
)}
</For>
</select>
<label> エリアの選択</label>
<select id="sel2" onChange={ selArea }>
<option value=''>-- エリアを選択 --</option>
<For each={hit_areas()}>{(area,idx)=>(
<option value={area.code}>{area.name}</option>
)}
</For>
</select>
<br/>
<h4>検索文字を入力してください</h4>
<input type="text" onInput={ bindWord } value={word()} />
<button type="button" onClick={ clear }>clear</button>
<div>ヒット数:{hit_cities?.length}件</div>
<ul class="ulDatas">
<For each={hit_cities}>{(city,idx)=>(
<li class="liData">
<label class="lb_hit" >
{city.name}
<label class="faList" on:click={()=> colorSet(city.id)}>
<Fa icon={faHeart} color={city.act} />
</label>
</label>
<br/>
<img class="imageStyle" src={setImage(imageSet(city.state,city.name))} />
</li>
)}
</For>
</ul>
</>
)
}
export default Tests;
スタイル制御
Solidのスタイル制御に用いるクラスプロパティは他と同様classとなります。また、CSSをインポートさせたい場合は、ダイレクトにCSSファイルをインポートするだけで反映できます。
import 'app.css';
<div class="hoge">
※モジュールを活用する方法もあります。その場合は以下のようになり、stylesオブジェクトからプロパティを展開し、リテラルで制御することになります(TailwindCSSなどもこれで用いる)。
import styles from 'app.module.css';
<div class={styles.hoge}>
動的にスタイルを反映させる
Solidで動的にスタイルを反映させる方法ですが、createSignalを使う方法とcreateStoreを使う方法があります。ステータス管理を制御する場合はcreateStoreの方が適切なので、以下のような記述方法を用いています。
今回は同コンポーネント内でのプロパティ制御なのでproduceの使用に適しています。任意のプロパティの値を変更したい場合は、分割代入を用いると便利です。
//スタイル制御
const colorSet = (id)=>{
let selidx = hit_cities.findIndex((v)=>{ return v.id === id }); //インデックス
const item = hit_cities[selidx];
let color = "";
if(item.act === active ){
color = "black";
}else{
color = "red";
}
setHitCities(
produce((hit_cities)=>{
hit_cities[selidx] = { ...item,...{act: color}};
})
);
}
on
createEffectで発動条件を限定したい場合、onという機能があります。今回はアイコンの色変化を元の都市リストへ反映させるために使用しています(利用条件があり、プリミティブな変数だけが有効となります)。
createEffect(on(target,(target)=>{
const selidx = cities.findIndex((v)=>{ return v.id === target.id });
setCities(produce((cities)=> cities[selidx] = target)); //一覧に反映
}))
reconcile
ストアオブジェクトを初期化する場合はreconcileを用いるのがセオリーのようです(それまでの依存関係を破棄できる)。reconcileは和訳でやり直しという意味だそうです(この記事にお似合いですな…)。
setHitCities(reconcile([])); //検索条件にヒットしない場合に初期化
以下作成中…