💡

SvelteでReactのforwardRef的なことのやり方

2023/08/18に公開6

カスタムコンポーネントにref propを渡して参照取りたい時にreactのforwardRef的なのがsvelteにないかなと思って、色々調べていくつか方法見つけたので、残しておきます。

Reactの場合

import * as React from 'react';

// Parent.jsx
const Parent = () => {
  const ref = React.useRef(null);
  
  React.useEffect(() => {
    if(ref.current) {
      ref.current.focus();
    }
  },[]);
  
  return (<Child ref={ref}/>);
}


// Child.jsx
const Child = React.forwardRef(function Child(props, ref) {
  return (<input ref={ref} />);
});

Svelteの場合

  1. writableを使う方法
<script>
	// Parent.svelte
	import Child from "./Child.svelte";

	let ref;

	$: {
		if($ref) {
			$ref.value = "hello from parent ";
		}
	}
</script>

<Child bind:ref={ref}/>
<script>
	// Child.svelte
	import { writable } from 'svelte/store';

	export const ref = writable();
</script>

<input bind:this={$ref}/>
  1. callback prop使う方法
<script>
	// Parent.svelte
	import Child from "./Child.svelte";
	import {onMount} from 'svelte';
	
	let ref;

	$: {
		if(ref) {
			ref.value = "hello from parent ";
		}
	}
	
</script>
<Child ref={(el) => (ref = el)}/>


<script>
	// Child.svelte
	import { writable } from 'svelte/store';
	import {onMount} from 'svelte';
	
	let el;
	export let ref;
	
	onMount(()=> {
		if(ref) {
			ref(el);
		}	
	})
	
</script>

<input bind:this={el}/>

Discussion

ryoppippiryoppippi

記事をありがとうございます。
こちら参考までに、Writableを使わなくてもbindingできます!
変数のbindingをするだけであればstoreはtoo muchかなと思いました。
https://svelte.dev/repl/2f507075ee0f44388745bb60692e60bd?version=4.2.0

qaynamqaynam

コメントありがとうございます。

そうですね、単純に値をバインディングしたいだけならそれでいいと思います。
記事の説明が悪かったですかね、どちらかというと子コンポーネントの参照が取りたいのが目的ですね。

多分👇のようになるかなと

<script>
	// Parent.svelte
	import Child from "./Child1.svelte";

	let ref;

	$: {
		if($ref) {
			$ref.current.element.value = "value changed from parent ";
		}
	}
</script>

<Child bind:ref={ref}/>
<button on:click={() => $ref.current.toggleTextVisibility()}>show child text</button>


<script>
	// Child.svelte
	import { writable } from 'svelte/store';
	import { onMount } from 'svelte';
	
	let currentRef;
	let showText= false;
	export const ref = writable(null);


	const toggleTextVisibility = () => {
		showText = !showText;
	}

	$: {
		console.log($ref);
	}

	onMount(()=> {
		console.log(ref);
		ref.set(
			{
			current: {
				element: currentRef,
				toggleTextVisibility
			}
		}
		);
	})
</script>

<input bind:this={currentRef}/>
{#if showText}
	<p>Text</p>
{/if}

親から直接子の関数を呼んだり、ステートを更新したりすることが目的ですね。

ryoppippiryoppippi

返信いただけて嬉しく思います。
Refにアクセスしたいという記事の趣旨は理解しております。
こちらこそ言葉足らずで申し訳ありません。

ただ、自分は数年svelteを使ってきて子要素のthisを直接参照する必要にかられたことがまずなかったです。
なので記事の例は必要以上に複雑性を上げているなという印象を持ちました(個人の感想です)。
なのでコメントさせていただきました。

上の例では

<script>
	// Parent.svelte
	import Child from "./Child.svelte";

	let showText = false

	let value = ""
</script>

<Child 
	bind:value
	bind:showText
/>
<button on:click={() => showText = !showText}>
	show child text
</button>
 <p>{value}</p>
<script>
	// Child.svelte
	export let value;
	export let showText= false;
</script>

<input bind:value/>
{#if showText}
	<p>Text</p>
{/if}

https://svelte.dev/repl/aea1115a9a3547e39f988dcee9a4bfa2?version=4.2.0

こんなかんじになるかなと
また、子の関数を直接呼ぶことも自分の経験ではなかったです。
あくまで、stateの変数を変化させると子の中でreactiveに処理が走る、という書き方をしていました。

もし具体例をいただけたらより詳細にお答えできると思います。
必要でなければ返信していただかなくても大丈夫です。お節介を失礼いたします。

qaynamqaynam

返信ありがとうございます。

自分もまだsvelteは初心者でして、ずっとreactをやってきたので、最初はreactのやり方に頭が回ってしまった気がします。

reactの場合に親で子のための状態を持つと、場合によって必要としないレンダリングが走ってしまい、それを防ぐためにキャッシュを使ったり、依存関係を変えたりと苦しみられる時があって、その時はforwardRefを使って子の状態をダイレクトに変えたりしていました。

確かに、そんな複雑な事をsvelteではやる必要はなさそうでした、これもsvelteの素晴らしいところですね。

単に子の関数だけ呼びたい場合に👇のように書いてもいける事がわかりました。

<script>
	// Parent.svelte
	import Child from "./Child.svelte";

	let toggleText = () => {};
</script>

<Child 
	bind:toggleText
/>
<button on:click={toggleText}>
	show child text
</button>

<script>
	// Child.svelte
       let showText= false;
       export const toggleText =  ()=> (showText = !showText);
</script>

{#if showText}
	<p>Text</p>
{/if}

ありがとうございました!

ryoppippiryoppippi

お久しぶりです。
そういえば

Parent.svelte
<script>
	import Child from "./Child.svelte";

	/** @type { Child } */
	let child;
</script>

<Child bind:this={child} />
<button on:click={child.toggleText}>
	show child text
</button>
Child.svelte
<script>
       let showText= false;

       export const toggleText =  ()=> (showText = !showText);
</script>

{#if showText}
	<p>Text</p>
{/if}

という書き方ができることを最近知りました。
最近になって(おそらくsvelte4から?)Svelte Componentに対してbind:thisを取得して、そのthisからmethodを取り出せるようになったそうです。
エディタでの補完も効くのでやってみてください

qaynamqaynam

お久しぶりです。

わざわざありがとう御座います。

この記事書いていた時に同じようなことを試していたような記憶もありますが、そのときはコンパイルエラーが出ていた気がします、昨日最新バージョンで試してみたら、本当にいけました!