setInterval は渡された関数を定期実行する関数です。
WindowOrWorkerGlobalScope.setInterval() – Web API | MDN
この定期実行される関数内で React の state を参照する処理を手なりで書くと次の様になります。
export default function App() {
// input 要素に入力された値
const [inputVal, setInputVal] = useState("");
// setInterval の中で整形された文字列
const [msg, setMsg] = useState("");
// マウント時にのみ実行される useEffect
useEffect(() => {
// 定期実行で input 要素に入力された値を外部に渡す
let callIndex = 0;
const intervalId = setInterval(() => {
setMsg(`index:${callIndex++} inputVal: ${inputVal}`);
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
return (
<div>
<input type="text" onChange={(e) => setInputVal(e.target.value)} />
{/* setInterval 内で作った文字列を表示 */}
<pre>{msg}</pre>
</div>
);
}
これを動かしたデモが次です。
残念ながら input 要素への入力が反映されません。これは setInterval に渡す関数を定義した時点で、その関数内における inputVal の値が固定されてしまうからです。これは React の関数コンポーネントがレンダリングをする時は、関数を実行する時であり、関数が実行される度にソースコード中の同じ場所で定義されている変数は異なる変数として扱われるためです。JavaScript 本来の挙動として全く正しいのですが state を単なる変数同様と考えてしまうと案外引っかかります。
const makeA = () => {
const a = Math.random();
return a;
}
const a1 = makeA();
const a2 = makeA(); // 2回目の実行で返って来た a と 1回目の実行で返って来た a は異なる値
これの対策として ref を使う方法があります。これは次の様に書けます。
import "./styles.css";
import { useState, useEffect, useRef } from "react";
export default function App() {
// input 要素に入力された値を ref で持つ
const inputVal = useRef("");
// setInterval の中で整形された文字列
const [msg, setMsg] = useState("");
// マウント時にのみ実行される useEffect
useEffect(() => {
// 定期実行で input 要素に入力された値を外部に渡す
// ref を使う
let callIndex = 0;
const intervalId = setInterval(() => {
setMsg(`index:${callIndex++} inputVal: ${inputVal.current}`);
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
return (
<div>
{/* ref を使う */}
<input
type="text"
onChange={(e) => (inputVal.current = e.target.value)}
/>
<pre>{msg}</pre>
</div>
);
}
↑のデモが次です。
無事、入力が反映されるようになりました。ref は reference の ref でその名の通り参照を保持できます。
Ref と DOM – React
フック API リファレンス – React#useRef
これでも目的は達成できるのですが、時には ref を介さない方がよい時もあります。例えば元々 state で定義されていて再描画にがっつり関わっている部分を無理に ref にしようとしだすと大変です。二重定義の方がましな事態にもなりえます。
state のみで逐次反映されるコードの例が次です。
import './styles.css';
import React, { useState, useEffect } from 'react';
export default function App() {
// input 要素に入力された値
const [inputVal, setInputVal] = useState('');
// 定期実行の中で整形された文字列
const [msg, setMsg] = useState('');
// interval 内の関数から外の useEffect に動くことを命令するための state
const [clock, setClock] = useState(Math.random);
// マウント時にのみ実行される useEffect
useEffect(() => {
// 定期的に clock を書き換えることによって下部で定義された useEffect を定期実行させる
const intervalId = setInterval(() => {
setClock(Math.random());
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
const [callIndex, setCallIndex] = useState(0);
// clock が書き変わるたびに実行される useEffect
// 上部の useEffect 内で定義された setInterval を起点に定期実行される
useEffect(() => {
setCallIndex((old) => old + 1);
// input 要素に入力された値を外部に渡す
setMsg(`index:${callIndex + 1} inputVal: ${inputVal}`);
}, [clock]);
return (
<div>
<input type="text" onChange={(e) => setInputVal(e.target.value)} />
{/* setInterval 内で作った文字列を表示 */}
<pre>{msg}</pre>
</div>
);
}
これのデモが次です。
先の ref 同様に定期実行される度に input 要素内の値が反映されます。これは setInterval 内には不変の値のみを渡し、外部では都度変わった値に応じた処理を行う様にしたためです。
この例では input 要素の value の保持方法を state にしました。このため入力の度に再レンダリングが走るようになります。これを利用して処理本体の useEffect を次の様に書き換えると定期実行に加えて入力による値の変化でも処理が実行される様になります。
useEffect(() => {
setCallIndex((old) => old + 1);
// input 要素に入力された値を外部に渡す
setMsg(`index:${callIndex + 1} inputVal: ${inputVal}`);
}, [clock, inputVal]);// inputVal を発火源に追加
// 以下の 3 点により入力の度に↑の処理が実行されます
// - state である inputVal が変わるたびに再レンダリング
// - 再レンダリングの度に useEffect の実行判定
// - useEffect の実行条件に inputVal の変化が含まれる
// @see https://ja.reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect