import { useState } from "react";
export default function Player() {
const [enteredPlayerName, setEnteredPlayerName] = useState('');
const [submitted, setSubmitted] = useState(false);
const handleChange = (e) => {
setEnteredPlayerName(e.target.value)
}
const handleClick = () => {
setSubmitted(true);
}
return (
<section id="player">
<h2>Welcome {submitted ? enteredPlayerName : 'unknown entity'}</h2>
<p>
<input type="text" value={enteredPlayerName} onChange={handleChange}/>
<button onClick={handleClick}>Set Name</button>
</p>
</section>
);
}
useRef
Hook을 이용하여 생성 가능.Ref는 여러 기능을 하지만, 가장 많이 사용되는 용법은 JSX와 연결(DOM)하는 기능이다.
const playerName = useRef();
...
<input ref={playerName} />
import { useState, useRef } from "react";
export default function Player() {
const playerName = useRef();
const [enteredPlayerName, setEnteredPlayerName] = useState('');
const handleClick = () => {
setEnteredPlayerName(playerName.current.value);
}
return (
<section id="player">
<h2>Welcome {enteredPlayerName ? enteredPlayerName : 'unknown entity'}</h2>
<p>
<input ref={playerName} type="text" />
<button onClick={handleClick}>Set Name</button>
</p>
</section>
);
}
[주의] useRef 사용 시 주의할 점
- 만약, 위 코드에서 input의 값을 비우고 싶다면 어떻게 해야할까?
-playerName.current.value = '';
와 같이 DOM을 직접 조작하게 된다면,
React의 '선언형' 코드 규칙에 위반된다.- 즉, 경우에 따라 state 대신 ref로 값을 읽어들이는 것은 괜찮지만, DOM을 직접 조작해서는 안된다. (state를 사용해야 함)
??
연산자enteredPlayerName ? enteredPlayerName : 'unknown entity'
라는 코드를 줄여서 표현 가능.??
)✅ State vs. Ref
- UI에 바로 적용되어야 하는 값들을
state
로 사용해야 함.- 시스템 내부에서만 보이는 값이거나, UI에 바로 적용되어선 안되는 값은
ref
로 사용해야 함.- 단, DOM에 직접적인 접근이 필요한 경우에는 ref를 사용하면 안된다.
export default function TimerChallenge ({ title, targetTime }) {
return (
<section className="challenge">
<h2>{title}</h2>
<p className="challenge-time">
{targetTime} Second{targetTime > 1 ? 's' : ''}
</p>
<p>
<button>
Start Challenge
</button>
</p>
<p className="active">
Time is running... / Timer inactive
</p>
</section>
)
}
import Player from './components/Player.jsx';
import TimerChallenge from './components/TimerChallenge.jsx';
function App() {
return (
<>
<Player />
<div id="challenges">
<TimerChallenge title="Easy" targetTime={1}/>
<TimerChallenge title="Not Easy" targetTime={5}/>
<TimerChallenge title="Getting Tough" targetTime={10}/>
<TimerChallenge title="Pros Only" targetTime={15}/>
</div>
</>
);
}
export default App;
timer
변수도 재할당되기 때문.// 일반 변수를 사용한 경우
import { useState } from "react";
export default function TimerChallenge ({ title, targetTime }) {
const [timerStarted, setTimerStarted] = useState(false);
const [timerExpired, setTimerExpired] = useState(false);
let timer;
const handleStart = () => {
timer = setTimeout(() => {
setTimerExpired(true);
}, targetTime * 1000);
setTimerExpired(false);
setTimerStarted(true);
}
const handleStop = () => {
setTimerStarted(false);
clearTimeout(timer);
}
return (
<section className="challenge">
<h2>{title}</h2>
{
timerExpired && <p>You Lost!</p>
}
<p className="challenge-time">
{targetTime} Second{targetTime > 1 ? 's' : ''}
</p>
<p>
<button onClick={timerStarted ? handleStop : handleStart}>
{timerStarted ? 'Stop Challenge' : 'Start Challenge'}
</button>
</p>
<p className={timerStarted ? 'active' : undefined}>
{timerStarted ? 'Time is running...' : 'Timer inactive'}
</p>
</section>
)
}
import { useState, useRef } from "react";
export default function TimerChallenge ({ title, targetTime }) {
const timer = useRef();
const [timerStarted, setTimerStarted] = useState(false);
const [timerExpired, setTimerExpired] = useState(false);
const handleStart = () => {
timer.current = setTimeout(() => {
setTimerExpired(true);
}, targetTime * 1000);
setTimerExpired(false);
setTimerStarted(true);
}
const handleStop = () => {
setTimerStarted(false);
clearTimeout(timer.current);
}
return (
<section className="challenge">
<h2>{title}</h2>
{
timerExpired && <p>You Lost!</p>
}
<p className="challenge-time">
{targetTime} Second{targetTime > 1 ? 's' : ''}
</p>
<p>
<button onClick={timerStarted ? handleStop : handleStart}>
{timerStarted ? 'Stop Challenge' : 'Start Challenge'}
</button>
</p>
<p className={timerStarted ? 'active' : undefined}>
{timerStarted ? 'Time is running...' : 'Timer inactive'}
</p>
</section>
)
}
export default function ResultModal({result, targetTime}) {
return <dialog className="result-modal">
<h2>You {result}</h2>
<p>
The target time was <strong>{targetTime}</strong> seconds.
</p>
<p>
You stopped the timer with <strong>X seconds</strong> left.
</p>
<form method="dialog">
<button>Close</button>
</form>
</dialog>
}
HTML dialog Element
- 참고 문서 - MDN
open
어트리뷰트를 지정해주어야 보이게 된다.
// TimerChallenge.jsx
import { useState, useRef } from "react";
import ResultModal from "./ResultModal";
export default function TimerChallenge ({ title, targetTime }) {
const timer = useRef();
const [timerStarted, setTimerStarted] = useState(false);
const [timerExpired, setTimerExpired] = useState(false);
const handleStart = () => {
timer.current = setTimeout(() => {
setTimerExpired(true);
}, targetTime * 1000);
setTimerExpired(false);
setTimerStarted(true);
}
const handleStop = () => {
setTimerStarted(false);
clearTimeout(timer.current);
}
return (
<>
{timerExpired && <ResultModal targetTime={targetTime} result="lost"/>}
<section className="challenge">
<h2>{title}</h2>
<p className="challenge-time">
{targetTime} Second{targetTime > 1 ? 's' : ''}
</p>
<p>
<button onClick={timerStarted ? handleStop : handleStart}>
{timerStarted ? 'Stop Challenge' : 'Start Challenge'}
</button>
</p>
<p className={timerStarted ? 'active' : undefined}>
{timerStarted ? 'Time is running...' : 'Timer inactive'}
</p>
</section>
</>
)
}
// ResultModal.jsx
export default function ResultModal({result, targetTime}) {
return <dialog className="result-modal" open>
<h2>You {result}</h2>
<p>
The target time was <strong>{targetTime} seconds.</strong>
</p>
<p>
You stopped the timer with <strong>X seconds</strong> left.
</p>
<form method="dialog">
<button>Close</button>
</form>
</dialog>
}
backdrop
어트리뷰트를 사용하기 위해서는 위와 같이 open인 상태에서는 접근 X.ref
를 이용해서 dialog DOM에 접근해서 사용 가능함.import { useState, useRef } from "react";
import ResultModal from "./ResultModal";
export default function TimerChallenge ({ title, targetTime }) {
const timer = useRef();
const dialog = useRef();
const [timerStarted, setTimerStarted] = useState(false);
const [timerExpired, setTimerExpired] = useState(false);
const handleStart = () => {
timer.current = setTimeout(() => {
setTimerExpired(true);
dialog.current.showModal(); // ✅ dialog.showModal Method
}, targetTime * 1000);
setTimerExpired(false);
setTimerStarted(true);
}
const handleStop = () => {
setTimerStarted(false);
clearTimeout(timer.current);
}
return (
<>
{timerExpired && <ResultModal ref={dialog} targetTime={targetTime} result="lost"/>}
<section className="challenge">
<h2>{title}</h2>
<p className="challenge-time">
{targetTime} Second{targetTime > 1 ? 's' : ''}
</p>
<p>
<button onClick={timerStarted ? handleStop : handleStart}>
{timerStarted ? 'Stop Challenge' : 'Start Challenge'}
</button>
</p>
<p className={timerStarted ? 'active' : undefined}>
{timerStarted ? 'Time is running...' : 'Timer inactive'}
</p>
</section>
</>
)
}
ref
를 받음. (부모에서 ref로 넘겨준 값)import { forwardRef } from "react";
const ResultModal = React.forwardRef(({result, targetTime}, ref) => {
return <dialog ref={ref} className="result-modal">
<h2>You {result}</h2>
<p>
The target time was <strong>{targetTime} seconds.</strong>
</p>
<p>
You stopped the timer with <strong>X seconds</strong> left.
</p>
<form method="dialog">
<button>Close</button>
</form>
</dialog>
})
export default ResultModal;
useImperativeHandle
HookuseImperativeHandle
- child component의 상태 변경을 parent component에서 하는 경우
- child component의 핸들러를 parent component에서 호출해야 하는 경우
- 자식 컴포넌트에서는 React.forwardRef로 부모 컴포넌트로부터 ref를 전달받아야 함.
-> 참고 문서
// ResultModal.jsx (Child Component)
import {forwardRef, useRef, useImperativeHandle} from "react";
const ResultModal = React.forwardRef(({result, targetTime}, ref) => {
const dialog = useRef();
// ✅ useImperativeHandle
useImperativeHandle(ref, () => {
return {
open() {
dialog.current.showModal();
}
};
});
return <dialog ref={ref} className="result-modal">
<h2>You {result}</h2>
<p>
The target time was <strong>{targetTime} seconds.</strong>
</p>
<p>
You stopped the timer with <strong>X seconds</strong> left.
</p>
<form method="dialog">
<button>Close</button>
</form>
</dialog>
})
export default ResultModal;
자식 컴포넌트에서 useImperativeHandle(ref, () => {})
를 해주면
부모 컴포넌트에서 해당 객체를 참고하여 자식 컴포넌트의 상태를 변경할 수 있음.
// TimerChallenge.jsx (Parent Component)
...
// 기존에는 dialog.current.showModal()로 사용했음
dialog.current.open();
// TimerChallenge.jsx
import { useState, useRef } from "react";
import ResultModal from "./ResultModal";
export default function TimerChallenge ({ title, targetTime }) {
const dialog = useRef();
const [timeRemaining, setTimeRemaining] = useState(targetTime * 1000);
const timerIsActive = timeRemaining > 0 && timeRemaining < targetTime * 1000;
if (timeRemaining <= 0) { // 시간 초과 시
clearInterval(timer.current);
setTimeRemaining(targetTime * 1000);
dialog.current.open();
}
const handleStart = () => {
timer.current = setInterval(() => {
setTimeRemaining(prevTimeRemaining => prevTimeRemaining - 10);
}, 10);
}
const handleStop = () => {
setTimerStarted(false);
clearInterval(timer.current);
}
return (
<>
<ResultModal ref={dialog} targetTime={targetTime} result="lost"/>
<section className="challenge">
<h2>{title}</h2>
<p className="challenge-time">
{targetTime} Second{targetTime > 1 ? 's' : ''}
</p>
<p>
<button onClick={timerIsActive ? handleStop : handleStart}>
{timerIsActive ? 'Stop Challenge' : 'Start Challenge'}
</button>
</p>
<p className={timerIsActive ? 'active' : undefined}>
{timerIsActive ? 'Time is running...' : 'Timer inactive'}
</p>
</section>
</>
)
}
// ResultModal.jsx
import {forwardRef, useRef, useImperativeHandle} from "react";
const ResultModal = React.forwardRef(({ targetTime, timeRemaining }, ref) => {
const dialog = useRef();
const userLost = timeRemaining <= 0;
const formattedRemainingTime = (timeRemaining / 1000).toFixed(2);
useImperativeHandle(ref, () => {
return {
open() {
dialog.current.showModal();
}
};
});
return <dialog ref={ref} className="result-modal">
{userLost && <h2>You Lost</h2>}
<p>
The target time was <strong>{targetTime} seconds.</strong>
</p>
<p>
You stopped the timer with <strong>{formattedRemainingTime} seconds</strong> left.
</p>
<form method="dialog">
<button>Close</button>
</form>
</dialog>
})
export default ResultModal;
if (timeRemaining <= 0) {
clearInterval(timer.current);
setTimeRemaining(targetTime * 1000); // Error Logic! (리셋을 여가서 해주면 X)
dialog.current.open();
}
const handleReset = () => {
setTimeRemaining(targetTime * 1000);
}
...
<ResultModal
ref={dialog}
targetTime={targetTime}
timeRemaining={timeRemaining}
onReset={handleReset}
/>
import {forwardRef, useRef, useImperativeHandle} from "react";
const ResultModal = React.forwardRef(({ targetTime, timeRemaining, onReset }, ref) => {
const dialog = useRef();
const userLost = timeRemaining <= 0;
const formattedRemainingTime = (timeRemaining / 1000).toFixed(2);
useImperativeHandle(ref, () => {
return {
open() {
dialog.current.showModal();
}
};
});
return <dialog ref={ref} className="result-modal">
{userLost && <h2>You Lost</h2>}
<p>
The target time was <strong>{targetTime} seconds.</strong>
</p>
<p>
You stopped the timer with <strong>{formattedRemainingTime} seconds</strong> left.
</p>
<form method="dialog" onSubmit={onReset}>
<button>Close</button>
</form>
</dialog>
})
export default ResultModal;
// ResultModal.jsx
import {forwardRef, useRef, useImperativeHandle} from "react";
const ResultModal = React.forwardRef(({ targetTime, timeRemaining, onReset }, ref) => {
const dialog = useRef();
const userLost = timeRemaining <= 0;
const formattedRemainingTime = (timeRemaining / 1000).toFixed(2);
const score = Math.round((1 - timeRemaining / (targetTime * 1000)) * 100);
useImperativeHandle(ref, () => {
return {
open() {
dialog.current.showModal();
}
};
});
return <dialog ref={ref} className="result-modal">
{userLost && <h2>You Lost</h2>}
{!userLost && <h2>Your Score: {score}</h2>}
<p>
The target time was <strong>{targetTime} seconds.</strong>
</p>
<p>
You stopped the timer with <strong>{formattedRemainingTime} seconds</strong> left.
</p>
<form method="dialog" onSubmit={onReset}>
<button>Close</button>
</form>
</dialog>
})
export default ResultModal;
onReset
이 트리거되도록 설정해야 함.onReset
을 바인딩해주면 됨.<dialog ref={dialog} onClose={onReset}>
...
</dialog>
createPortal
로 감싸서 두번째 인자로 HTML Element를 넣어주면 됨.import { forwardRef, useRef, useImperativeHandle } from "react";
import { createPortal } from 'react-dom';
const ResultModal = React.forwardRef(({ targetTime, timeRemaining, onReset }, ref) => {
...
return createPortal(<dialog ref={ref} className="result-modal" onClose={onReset}>
{userLost && <h2>You Lost</h2>}
{!userLost && <h2>Your Score: {score}</h2>}
<p>
The target time was <strong>{targetTime} seconds.</strong>
</p>
<p>
You stopped the timer with <strong>{formattedRemainingTime} seconds</strong> left.
</p>
<form method="dialog" onSubmit={onReset}>
<button>Close</button>
</form>
</dialog>, document.getElementById('modal'))
})
export default ResultModal;