はじめに
皆さんSvelte使ってますか。
https://svelte.dev/
Svelteは少ないコード量でハイパフォーマンスが出せるReact、Vueに次ぐ新しいフレームワークです。
非常に便利で重宝しているのですが、実務で使うにあたりちょっと足りない機能があったりします。
Reactだと標準機能になっている部分をちょっとした小技で補っているのでいくつかご紹介します。
小技集
オブジェクトでのstyle指定
Reactだと出来たオブジェクトでのstyle指定がなぜかSvelteだとできないのです。
<div style={{ width: 100, display:'flex' }} />
Svelteの場合は以下のように通常とHTMLと同じように文字列で指定することしかできません。
他にもクラス名+CSSでのスタイルを記述することも可能ですが、インラインで書きたいときには不便です。
<div style="width:100px;display:flex" />
そんな私が使っているのが、オブジェクトStyleを文字列に変換してくれる関数です。
import type { CSSObject } from '@emotion/css';
const keyWithPx: (keyof CSSObject)[] = [
'width',
'maxWidth',
'minWidth',
'height',
'minHeight',
'maxHeight',
'top',
'bottom',
'left',
'right',
'fontSize',
'padding',
'margin',
'marginTop',
'marginRight',
'marginBottom',
'marginLeft',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
'borderRadius',
'outlineOffset'
];
export const styleToString = (style: CSSObject) => {
return Object.keys(style).reduce((acc, key) => {
const value = style[key];
const styleKey = key
.split(/(?=[A-Z])/)
.join('-')
.toLowerCase();
return (
acc +
(value != null
? `${styleKey}:${
typeof value === 'number' && keyWithPx.includes(key)
? `${value}px`
: value
};`
: '')
);
}, '');
};
export class Style {
cssObject: CSSObject;
constructor(cssObject?: CSSObject) {
this.cssObject = cssObject ?? {};
}
toString() {
return styleToString(this.cssObject);
}
}
これによって以下のような記述が可能になります。
Reactより多少長いですが、型補完も効くので便利です。
width
、display
などが詰まった連想配列の型は@emotion/css
からお借りしています。
Reactと同じように、width
、height
などは数値でも指定できるようにしています。
<div style={new Style({ width: 100, display: 'flex' }).toString()} />
ReactPortal
Reactには親コンポーネントのDOM階層外にあるDOMノードに対して子コンポーネントをレンダーするための仕組みを提供してくれています。
https://ja.reactjs.org/docs/portals.html
UIコンポーネントでいうところのモーダルなどを作る際に便利だったりしますが、Svelteの公式からはこのような機能は提供されていません。
私はこのような独自のPortalコンポーネントで補っています。
<script context="module" lang="ts">
import { tick } from 'svelte';
type Target = HTMLElement | string;
export function portal(el: HTMLElement, target: Target = 'body') {
let targetEl;
async function update(newTarget: Target) {
target = newTarget;
if (typeof target === 'string') {
targetEl = document.querySelector(target);
if (targetEl === null) {
await tick();
targetEl = document.querySelector(target);
}
if (targetEl === null) {
throw new Error('no element found');
}
} else if (target instanceof HTMLElement) {
targetEl = target;
} else {
throw new TypeError();
}
targetEl.appendChild(el);
el.hidden = false;
}
function destroy() {
if (el.parentNode) {
el.parentNode.removeChild(el);
}
}
update(target);
return {
update,
destroy
};
}
</script>
<script lang="ts">
export let target: HTMLElement | string = 'body';
</script>
<div use:portal={target} hidden>
<slot />
</div>
Svelteのディレクティブ機能を使って実装をしています。
コンポーネントのtarget
プロパティに渡したDOMのに、子コンポーネントがレンダリングされます。
以下のように使うことができます。
この場合はTooltipコンポーネントがdivの直下ではなく、bodyの直下に描画されます。
<div>
<Portal target={document.body}>
<Tooltip />
</Portal>
</div>
その他
SvelteにはErrorBoundaryの機能がありません。(ReactでいうところのSuspenseコンポーネント)
そのため、一部でエラーが出た際にはそのエラーがルートのコンポーネントまで伝わり、画面全体が固まります。
これについては、様々な解決策を出しているサイトを見ましたがどれも上手くいかないので、現状改善は難しいと思われます。
本格的な対策は公式の対応を待つとしても、なるべくエラーを起こさないことがとりあえずの対策になるかと思います。
私のお勧めはTypeScriptを使うのは大前提ですが、中でもtsconfigのnoUncheckedIndexedAccess
をtrue
にすると良いと思います。
これは連想配列などでプロパティが存在しない可能性も考慮して、プロパティの中身をxxx | undefined
という型に推論してくれるというものになります。
個人的な感覚ですが、TypeScriptで書いても発生するJavaScriptエラーの大半がこの型の不一致によるものだと思うので、この設定を追加することで多くのエラーをコンパイル時に見つけることができます。
const obj: Record<string, string> = {};
obj1.foo;
// on : string | undefined
// off: string
おわりに
いかがでしたでしょうか。
他にも小技を見つけ次第追記していこうと思います。