Reactã®useFormStatusããã¯ã䜿çšãã鲿æšå®ãšå®äºæéäºæž¬ã®å®è£ æ¹æ³ãåŠã³ãããŒã¿éã®å€ãã¢ããªã±ãŒã·ã§ã³ã®ãŠãŒã¶ãŒäœéšãåäžãããŸãã
React useFormStatus 鲿æšå®ïŒå®äºæéã®äºæž¬
React 18ã§å°å
¥ãããReactã®useFormStatusããã¯ã¯ããã©ãŒã éä¿¡ã®ã¹ããŒã¿ã¹ã«é¢ãã貎éãªæ
å ±ãæäŸããŸããããã¯çŽæ¥çã«é²ææšå®ãæäŸãããã®ã§ã¯ãããŸãããããã®ããããã£ãä»ã®æè¡ã掻çšããŠãå®è¡ã«æéããããå¯èœæ§ã®ãããã©ãŒã éä¿¡äžã«ãŠãŒã¶ãŒã«ææçŸ©ãªãã£ãŒãããã¯ãæäŸã§ããŸãããã®èšäºã§ã¯ãuseFormStatusã䜿çšããéã®é²ææšå®ãšå®äºæéäºæž¬ã®æ¹æ³ãæ¢ããããé
åçã§ãŠãŒã¶ãŒãã¬ã³ããªãŒãªäœéšãå®çŸããŸãã
useFormStatusã®çè§£
鲿æšå®ã«å
¥ãåã«ãuseFormStatusã®ç®çãç°¡åã«æ¯ãè¿ã£ãŠã¿ãŸãããããã®ããã¯ã¯ãactionãããããå©çšãã<form>èŠçŽ å
ã§äœ¿çšãããããã«èšèšãããŠããŸãã以äžã®ããããã£ãå«ããªããžã§ã¯ããè¿ããŸãïŒ
pending: ãã©ãŒã ãçŸåšéä¿¡äžãã©ããã瀺ãããŒã«å€ãdata: ãã©ãŒã ã§éä¿¡ãããããŒã¿ïŒéä¿¡ãæåããå ŽåïŒãmethod: ãã©ãŒã éä¿¡ã«äœ¿çšãããHTTPã¡ãœããïŒäŸïŒãPOSTãããGETãïŒãaction: ãã©ãŒã ã®actionããããã«æž¡ããã颿°ãerror: éä¿¡ã倱æããå Žåã®ãšã©ãŒãªããžã§ã¯ãã
useFormStatusã¯ãã©ãŒã ãéä¿¡äžãã©ãããæããŠãããŸãããç¹ã«action颿°ãè€éãŸãã¯é·æéã®æäœãå«ãå Žåãéä¿¡ã®é²æã«é¢ããçŽæ¥çãªæ
å ±ã¯æäŸããŸããã
鲿æšå®ã®èª²é¡
äžå¿çãªèª²é¡ã¯ãaction颿°ã®å®è¡ãReactã«ãšã£ãŠäžéæã§ãããšããäºå®ã«ãããŸããç§ãã¡ã¯æ¬è³ªçã«ããã»ã¹ã®ã©ã®æ®µéã«ããã®ããç¥ãããšãã§ããŸãããããã¯ç¹ã«ãµãŒããŒãµã€ãã®æäœã«åœãŠã¯ãŸããŸãããããããã®å¶çŽãå
æããããã«æ§ã
ãªæŠç¥ãçšããããšãã§ããŸãã
鲿æšå®ã®æŠç¥
以äžã«ãããããã«ãã¬ãŒããªããããããã€ãã®ã¢ãããŒãã玹ä»ããŸãïŒ
1. Server-Sent Events (SSE) ãŸã㯠WebSockets
æãå ç¢ãªè§£æ±ºçã¯ããµãŒããŒããã¯ã©ã€ã¢ã³ãã«é²ææŽæ°ãããã·ã¥ããããšã§ããããã¯ä»¥äžã䜿çšããŠå®çŸã§ããŸãïŒ
- Server-Sent Events (SSE): ãµãŒããŒããã¯ã©ã€ã¢ã³ããžã®åäžHTTPæ¥ç¶ãä»ããŠæŽæ°ãããã·ã¥ã§ããåæ¹åïŒãµãŒããŒããã¯ã©ã€ã¢ã³ãïŒãããã³ã«ã§ããSSEã¯ãã¯ã©ã€ã¢ã³ããæŽæ°ã*åä¿¡*ããã ãã§ããå Žåã«çæ³çã§ãã
- WebSockets: ã¯ã©ã€ã¢ã³ããšãµãŒããŒéã«æç¶çãªæ¥ç¶ãæäŸããåæ¹åéä¿¡ãããã³ã«ã§ããWebSocketsã¯ãåæ¹åã®ãªã¢ã«ã¿ã€ã æŽæ°ã«é©ããŠããŸãã
äŸ (SSE):
ãµãŒããŒãµã€ã (Node.js):
const express = require('express');
const app = express();
app.get('/progress', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
let progress = 0;
const interval = setInterval(() => {
progress += 10;
if (progress > 100) {
progress = 100;
clearInterval(interval);
res.write(`data: {"progress": ${progress}, "completed": true}\n\n`);
res.end();
} else {
res.write(`data: {"progress": ${progress}, "completed": false}\n\n`);
}
}, 500); // Simulate progress update every 500ms
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
ã¯ã©ã€ã¢ã³ããµã€ã (React):
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
useEffect(() => {
const eventSource = new EventSource('/progress');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
setProgress(data.progress);
if (data.completed) {
eventSource.close();
}
};
eventSource.onerror = (error) => {
console.error('EventSource failed:', error);
eventSource.close();
};
return () => {
eventSource.close();
};
}, []);
return (
<div>
<p>Progress: {progress}%</p>
</div>
);
}
export default MyComponent;
解説:
- ãµãŒããŒã¯SSEçšã®é©åãªããããŒãèšå®ããŸãã
- ãµãŒããŒã¯é²ææŽæ°ã
data:ã€ãã³ããšããŠéä¿¡ããŸããåã€ãã³ãã¯progressãšcompletedãã©ã°ãå«ãJSONãªããžã§ã¯ãã§ãã - Reactã³ã³ããŒãã³ãã¯
EventSourceã䜿çšããŠãããã®ã€ãã³ãããªãã¹ã³ããŸãã - ã³ã³ããŒãã³ãã¯åä¿¡ããã€ãã³ãã«åºã¥ããŠç¶æ
ïŒ
progressïŒãæŽæ°ããŸãã
å©ç¹: æ£ç¢ºãªé²ææŽæ°ããªã¢ã«ã¿ã€ã ã®ãã£ãŒãããã¯ã
æ¬ ç¹: ãµãŒããŒãµã€ãã®å€æŽãå¿ èŠãããè€éãªå®è£ ã
2. APIãšã³ããã€ã³ãã«ããããŒãªã³ã°
SSEãWebSocketsã䜿çšã§ããªãå Žåã¯ãããŒãªã³ã°ãå®è£ ã§ããŸããã¯ã©ã€ã¢ã³ãã¯å®æçã«ãµãŒããŒã«ãªã¯ãšã¹ããéä¿¡ããŠãæäœã®ã¹ããŒã¿ã¹ã確èªããŸãã
äŸ:
ãµãŒããŒãµã€ã (Node.js):
const express = require('express');
const app = express();
// Simulate a long-running task
let taskProgress = 0;
let taskId = null;
app.post('/start-task', (req, res) => {
taskProgress = 0;
taskId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); // Generate a unique task ID
// Simulate background processing
const interval = setInterval(() => {
taskProgress += 10;
if (taskProgress >= 100) {
taskProgress = 100;
clearInterval(interval);
}
}, 500);
res.json({ taskId });
});
app.get('/task-status/:taskId', (req, res) => {
if (req.params.taskId === taskId) {
res.json({ progress: taskProgress });
} else {
res.status(404).json({ message: 'Task not found' });
}
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
ã¯ã©ã€ã¢ã³ããµã€ã (React):
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [taskId, setTaskId] = useState(null);
const startTask = async () => {
const response = await fetch('/start-task', { method: 'POST' });
const data = await response.json();
setTaskId(data.taskId);
};
useEffect(() => {
if (!taskId) return;
const interval = setInterval(async () => {
const response = await fetch(`/task-status/${taskId}`);
const data = await response.json();
setProgress(data.progress);
if (data.progress === 100) {
clearInterval(interval);
}
}, 1000); // Poll every 1 second
return () => clearInterval(interval);
}, [taskId]);
return (
<div>
<button onClick={startTask} disabled={taskId !== null}>Start Task</button>
{taskId && <p>Progress: {progress}%</p>}
</div>
);
}
export default MyComponent;
解説:
- ã¯ã©ã€ã¢ã³ãã¯
/start-taskãåŒã³åºããŠã¿ã¹ã¯ãéå§ããtaskIdãåãåããŸãã - ãã®åŸãã¯ã©ã€ã¢ã³ãã¯
/task-status/:taskIdã宿çã«ããŒãªã³ã°ããŠé²æç¶æ³ãååŸããŸãã
å©ç¹: æ¯èŒçå®è£ ãç°¡åã§ãæç¶çãªæ¥ç¶ãå¿ èŠãšããŸããã
æ¬ ç¹: SSE/WebSocketsããã粟床ãäœãå¯èœæ§ããããããŒãªã³ã°ééã«ããé å»¶ãçºçããé »ç¹ãªãªã¯ãšã¹ãã«ãããµãŒããŒã«è² è·ãããããŸãã
3. æ¥œèŠ³çæŽæ°ãšãã¥ãŒãªã¹ãã£ã¯ã¹
å Žåã«ãã£ãŠã¯ãæ¥œèŠ³çæŽæ°ãšãã¥ãŒãªã¹ãã£ã¯ã¹ãçµã¿åãããŠã劥åœãªæšå®å€ãæäŸããããšãã§ããŸããäŸãã°ããã¡ã€ã«ãã¢ããããŒãããŠããå Žåãã¯ã©ã€ã¢ã³ããµã€ãã§ã¢ããããŒãããããã€ãæ°ã远跡ããç·ãã¡ã€ã«ãµã€ãºã«åºã¥ããŠé²æãæšå®ã§ããŸãã
äŸ (ãã¡ã€ã«ã¢ããããŒã):
import React, { useState } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [file, setFile] = useState(null);
const handleFileChange = (event) => {
setFile(event.target.files[0]);
};
const handleSubmit = async (event) => {
event.preventDefault();
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentage = Math.round((event.loaded * 100) / event.total);
setProgress(percentage);
}
});
xhr.open('POST', '/upload'); // Replace with your upload endpoint
xhr.send(formData);
xhr.onload = () => {
if (xhr.status === 200) {
console.log('Upload complete!');
} else {
console.error('Upload failed:', xhr.status);
}
};
xhr.onerror = () => {
console.error('Upload failed');
};
} catch (error) {
console.error('Upload error:', error);
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input type="file" onChange={handleFileChange} />
<button type="submit" disabled={!file}>Upload</button>
</form>
<p>Progress: {progress}%</p>
</div>
);
}
export default MyComponent;
解説:
- ã³ã³ããŒãã³ãã¯
XMLHttpRequestãªããžã§ã¯ãã䜿çšããŠãã¡ã€ã«ãã¢ããããŒãããŸãã xhr.uploadã®progressã€ãã³ããªã¹ããŒã䜿çšããŠãã¢ããããŒãã®é²æã远跡ããŸãã- ã€ãã³ãã®
loadedããããã£ãštotalããããã£ã䜿çšããŠãå®äºçãèšç®ããŸãã
å©ç¹: ã¯ã©ã€ã¢ã³ããµã€ãã®ã¿ã§å®çµãã峿ã®ãã£ãŒãããã¯ãæäŸã§ããŸãã
æ¬ ç¹: 粟床ã¯ãã¥ãŒãªã¹ãã£ã¯ã¹ã®ä¿¡é Œæ§ã«äŸåãããã¹ãŠã®ã¿ã€ãã®æäœã«é©ããŠãããšã¯éããŸããã
4. ã¢ã¯ã·ã§ã³ãããå°ããªã¹ãããã«åå²ãã
action颿°ãè€æ°ã®ç°ãªãã¹ããããå®è¡ããå Žåãåã¹ãããã®åŸã«UIãæŽæ°ããŠé²æã瀺ãããšãã§ããŸããããã«ã¯ãæŽæ°ãæäŸããããã«action颿°ãä¿®æ£ããå¿
èŠããããŸãã
äŸ:
import React, { useState } from 'react';
async function myAction(setProgress) {
setProgress(10);
await someAsyncOperation1();
setProgress(40);
await someAsyncOperation2();
setProgress(70);
await someAsyncOperation3();
setProgress(100);
}
function MyComponent() {
const [progress, setProgress] = useState(0);
const handleSubmit = async () => {
await myAction(setProgress);
};
return (
<div>
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
<p>Progress: {progress}%</p>
</div>
);
}
export default MyComponent;
解説:
myAction颿°ã¯setProgressã³ãŒã«ããã¯ãåãå ¥ããŸãã- å®è¡äžã®æ§ã ãªæç¹ã§é²æç¶æ ãæŽæ°ããŸãã
å©ç¹: é²ææŽæ°ãçŽæ¥å¶åŸ¡ã§ããŸãã
æ¬ ç¹: action颿°ãä¿®æ£ããå¿
èŠããããã¹ãããã容æã«åå²ã§ããªãå Žåã¯å®è£
ãããè€éã«ãªãå¯èœæ§ããããŸãã
å®äºæéã®äºæž¬
é²ææŽæ°ãåŸããããããããã䜿çšããŠæ®ãã®æšå®æéãäºæž¬ã§ããŸããç°¡åãªã¢ãããŒãã¯ãç¹å®ã®é²æã¬ãã«ã«å°éãããŸã§ã«ããã£ãæéã远跡ããããã倿¿ããŠåèšæéãæšå®ããããšã§ãã
äŸ (ç°¡æç):
import React, { useState, useEffect, useRef } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState(null);
const startTimeRef = useRef(null);
useEffect(() => {
if (progress > 0 && startTimeRef.current === null) {
startTimeRef.current = Date.now();
}
if (progress > 0) {
const elapsedTime = Date.now() - startTimeRef.current;
const estimatedTotalTime = (elapsedTime / progress) * 100;
const remainingTime = estimatedTotalTime - elapsedTime;
setEstimatedTimeRemaining(Math.max(0, remainingTime)); // Ensure non-negative
}
}, [progress]);
// ... (rest of the component and progress updates as described in previous sections)
return (
<div>
<p>Progress: {progress}%</p>
{estimatedTimeRemaining !== null && (
<p>Estimated Time Remaining: {Math.round(estimatedTimeRemaining / 1000)} seconds</p>
)}
</div>
);
}
export default MyComponent;
解説:
- 鲿ãæåã«æŽæ°ããããšãã®éå§æå»ãä¿åããŸãã
- çµéæéãèšç®ããããã䜿çšããŠåèšæéãæšå®ããŸãã
- æšå®åèšæéããçµéæéãåŒãããšã§ãæ®ãæéãèšç®ããŸãã
éèŠãªèæ ®äºé :
- 粟床: ããã¯*éåžžã«*åçŽåãããäºæž¬ã§ãããããã¯ãŒã¯ç¶æ³ããµãŒããŒã®è² è·ããã®ä»ã®èŠå ã粟床ã«å€§ãã圱é¿ããå¯èœæ§ããããŸããè€æ°ã®ééã§å¹³åãåããªã©ãããæŽç·Žãããæè¡ã§ç²ŸåºŠãåäžãããããšãã§ããŸãã
- èŠèŠçãã£ãŒãããã¯: æéã*æšå®å€*ã§ããããšãæç¢ºã«ç€ºããŸããç¯å²ïŒäŸïŒãæ®ãæšå®æéïŒ5ïœ10ç§ãïŒã衚瀺ãããšãããçŸå®çã«ãªããŸãã
- ãšããžã±ãŒã¹: åæã®é²æãéåžžã«é ããšããžã±ãŒã¹ãåŠçããŸãããŒãé€ç®ãé床ã«å€§ããªæšå®å€ã衚瀺ããã®ãé¿ããŠãã ããã
useFormStatusãšé²ææšå®ã®çµã¿åãã
useFormStatusèªäœã¯é²ææ
å ±ãæäŸããŸãããããã®pendingããããã£ã䜿çšããŠé²æã€ã³ãžã±ãŒã¿ãæå¹ãŸãã¯ç¡å¹ã«ããããšãã§ããŸããäŸïŒ
import React, { useState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (Progress estimation logic from previous examples)
function MyComponent() {
const [progress, setProgress] = useState(0);
const { pending } = useFormStatus();
const handleSubmit = async (formData) => {
// ... (Your form submission logic, including updates to progress)
};
return (
<form action={handleSubmit}>
<button type="submit" disabled={pending}>Submit</button>
{pending && <p>Progress: {progress}%</p>}
</form>
);
}
ãã®äŸã§ã¯ã鲿ã€ã³ãžã±ãŒã¿ã¯ãã©ãŒã ãä¿çäžïŒã€ãŸããuseFormStatus.pendingãtrueã®éïŒã«ã®ã¿è¡šç€ºãããŸãã
ãã¹ããã©ã¯ãã£ã¹ãšèæ ®äºé
- 粟床ãåªå ãã: å®è¡ãããæäœã®ã¿ã€ãã«é©ãã鲿æšå®æè¡ãéžæããŸããSSE/WebSocketsã¯äžè¬çã«æãæ£ç¢ºãªçµæãæäŸããŸãããããåçŽãªã¿ã¹ã¯ã«ã¯ãã¥ãŒãªã¹ãã£ã¯ã¹ã§ååãªå ŽåããããŸãã
- æç¢ºãªèŠèŠçãã£ãŒãããã¯ãæäŸãã: ããã°ã¬ã¹ããŒãã¹ãããŒããã®ä»ã®èŠèŠçãªåå³ã䜿çšããŠãæäœãé²è¡äžã§ããããšã瀺ããŸãã鲿ã€ã³ãžã±ãŒã¿ãšã該åœããå Žåã¯æ®ãæšå®æéã«æç¢ºã«ã©ãã«ãä»ããŸãã
- ãšã©ãŒãé©åã«åŠçãã: æäœäžã«ãšã©ãŒãçºçããå Žåã¯ããŠãŒã¶ãŒã«æçãªãšã©ãŒã¡ãã»ãŒãžã衚瀺ããŸãã鲿ã€ã³ãžã±ãŒã¿ãç¹å®ã®ããŒã»ã³ããŒãžã§åæ¢ãããŸãŸã«ãªããªãããã«ããŸãã
- ããã©ãŒãã³ã¹ãæé©åãã: UIã¹ã¬ããã§èšç®ã³ã¹ãã®é«ãæäœãå®è¡ãããšãããã©ãŒãã³ã¹ã«æªåœ±é¿ãåãŒãå¯èœæ§ãããããé¿ããŠãã ãããWebã¯ãŒã«ãŒãä»ã®æè¡ã䜿çšããŠãäœæ¥ãããã¯ã°ã©ãŠã³ãã¹ã¬ããã«ãªãããŒãããŸãã
- ã¢ã¯ã»ã·ããªãã£: 鲿ã€ã³ãžã±ãŒã¿ãé害ãæã€ãŠãŒã¶ãŒã«ãã¢ã¯ã»ã¹å¯èœã§ããããšã確èªããŸããARIA屿§ã䜿çšããŠãæäœã®é²æã«é¢ããæå³æ
å ±ãæäŸããŸããäŸãã°ãããã°ã¬ã¹ããŒã§
aria-valuenowãaria-valueminãaria-valuemaxã䜿çšããŸãã - ããŒã«ã©ã€ãŒãŒã·ã§ã³: æ®ãæšå®æéã衚瀺ããéã¯ãç°ãªãæé圢åŒãå°åã®å¥œã¿ã«æ³šæããŠãã ããã
date-fnsãmoment.jsã®ãããªã©ã€ãã©ãªã䜿çšããŠããŠãŒã¶ãŒã®ãã±ãŒã«ã«åãããŠæéãé©åã«ãã©ãŒãããããŸãã - åœéå: ãšã©ãŒã¡ãã»ãŒãžããã®ä»ã®ããã¹ãã¯ãè€æ°ã®èšèªããµããŒãããããã«åœéåããå¿
èŠããããŸãã
i18nextã®ãããªã©ã€ãã©ãªã䜿çšããŠç¿»èš³ã管çããŸãã
çµè«
Reactã®useFormStatusããã¯ã¯çŽæ¥çã«é²ææšå®æ©èœãæäŸããŸããããä»ã®æè¡ãšçµã¿åãããããšã§ããã©ãŒã éä¿¡äžã«ãŠãŒã¶ãŒã«ææçŸ©ãªãã£ãŒãããã¯ãæäŸã§ããŸããSSE/WebSocketsãããŒãªã³ã°ãæ¥œèŠ³çæŽæ°ããŸãã¯ã¢ã¯ã·ã§ã³ãå°ããªã¹ãããã«åå²ããããšã«ãã£ãŠãããé
åçã§ãŠãŒã¶ãŒãã¬ã³ããªãŒãªäœéšãåµãåºãããšãã§ããŸãã粟床ãåªå
ããæç¢ºãªèŠèŠçãã£ãŒãããã¯ãæäŸãããšã©ãŒãé©åã«åŠçããããã©ãŒãã³ã¹ãæé©åããŠãå Žæãèæ¯ã«é¢ããããã¹ãŠã®ãŠãŒã¶ãŒã«è¯å®çãªäœéšãä¿èšŒããããšãå¿ããªãã§ãã ããã