- Accessing DOM Elements with Refs
- Managing Values with Refs
- Exposing API Functions from Components
- Detaching DOM Rendering from JSX Structure with Portals
Refs
Refs를 쓰기 전 코드
import {useState} from "react";
export default function Player() {
const[enteredPlayerName, setEnteredPlayerName] = useState(null);
const[submitted, setSubmitted] = useState(false);
function handleChange(event){
setSubmitted(false);
setEnteredPlayerName(event.target.value)
}
function handleClick(){
setSubmitted(true);
}
return (
<section id="player">
<h2>Welcome {submitted ? enteredPlayerName : 'unknown entity' } </h2>
<p>
<input type="text" onChange={handleChange} value={enteredPlayerName}/>
<button onClick={handleClick}>Set Name</button>
</p>
</section>
);
}
submit 후 타이핑시 unknown entity로 표시되는 문제점이 있음.
코드가 김.
Refs 적용 코드
import {useState, useRef} from "react";
export default function Player() {
const playerName = useRef();
const[enteredPlayerName, setEnteredPlayerName] = useState(null);
function handleClick(){
setEnteredPlayerName(playerName.current.value);
}
return (
<section id="player">
<h2>Welcome {enteredPlayerName ?? 'unknown entity' } </h2>
<p>
<input ref = {playerName} type="text"/>
<button onClick={handleClick}>Set Name</button>
</p>
</section>
);
}
Refs를 통해 JSX요소와 연결하기 (ref 속성 사용)
useRef()를 통해 받는 참조값은 항상 자바스크립트 객체이며, current속성을 가지고 있음.
.current를 통해 실제 참조값에 접근
`??`구문 : 자바스크립트의 "null 병합 연산자"
좌측 피연산자가 `null` 또는 `undefined`일 경우 우측 피연산자를 반환. 그렇지 않으면 좌측 피연산자를 반환
DOM을 직접 조작하기보다는 선언적 코드를 사용하도록 하자! React가 수행하도록!
State vs Refs
상태를 왜 사용할까?
상태가 없다면(Ref만 사용한다면) 초기에 연결값이 없는 것은 물론, 참조값이 바뀔때마다 컴포넌트가 재실행되지 않음.
상태를 업데이트하면, 컴포넌트를 재실행함!
State
- Causes component re-evaluation (re-execution) with change
- Should be used for values that are directly reflected in the UI
- Should not be used for "behind the scenes" values that have no direct UI impact
Refs
- Do not cause component re-evaluation when changed
- Can be used to gain direct DOM element access (-> great for reading values or accessing certain browser APIs)
className = " " 와 className = { } 의 차이점.
정적 문자열로 클래스를 설정 / JavaScript 표현식을 사용하여 동적으로 클래스 이름을 설정
""로 비워두는 것은 괜찮지만, {} 로 비워주면 정의되지 않았다는 에러가 발생!
TimerChallenge.jsx
import {useState} from "react";
let timer; // 상태 업데이트 시 컴포넌트 재실행 => timer 변수 재생성 -> 컴포넌트 바깥에서 정의
// timer가 현재 모든 인스턴스에 공유되고 있으므로, 1초,5초 게임 실행시 덮어씌워짐.
export default function TimerChallenge({title, targetTime}) {
const [timerExpired, setTimerExpired] = useState(false);
const [timerStarted, setTimerStarted] = useState(false);
function handleStart() {
timer = setTimeout(() => {
setTimerExpired(true);
}, targetTime * 1000);
setTimerStarted(true);
}
function handleStop(){
clearTimeout(timer) // 포인터 : timer의 id를 input으로 필요
}
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' : 'Start'} Challenge
</button>
</p>
<p className={timerStarted ? 'active' : undefined}>
{timerStarted ? "Time is running..." : "Timer inactive"}
</p>
</section>
);
}
timer 객체를 여러 인스턴스에서 공유하는 문제점 발생
const timer = useRef();
const [timerExpired, setTimerExpired] = useState(false);
const [timerStarted, setTimerStarted] = useState(false);
function handleStart() {
timer.current = setTimeout(() => {
setTimerExpired(true);
}, targetTime * 1000);
setTimerStarted(true);
}
function handleStop(){
clearTimeout(timer.current) // 포인터 : timer의 id를 input으로 필요
}
각자의 timer를 가지고, 기억
dialog backdrop이란?
HTML에서 <dialog> 요소가 활성화될 때 나타나는 배경
Ref를 다른 컴포넌트로 전달할 수 없다. 또한 해당 컴포넌트의 요소로도 전달할 수 없다.
forwardRef를 통해서 컴포넌트에서 컴포넌트로 참조를 전달해 사용할 수 있다.
이를 사용하려면 컴포넌트 함수를 감싸주어야함.
참조 속성을 받을 때에는
첫번째 매개변수로로 구조 분해 할당가능한 속성이 오며, 두 번째 매개변수로 ref 매개변수를 받는다.
import {forwardRef, useImperativeHandle, useRef} from "react";
import {createPortal} from 'react-dom';
const ResultModal = forwardRef(function ResultModal(
{ targetTime, remainingTime, onReset},
ref
) {
const dialog = useRef();
const userLost = remainingTime <=0;
const formattedRemainingTime = (remainingTime / 1000).toFixed(2);
const score = Math.round((1 - remainingTime / (targetTime*1000))*100);
useImperativeHandle(ref, () => {
return {
open(){
dialog.current.showModal(); // TimerChalleng 와 ResultModal 컴포넌트를 분리하기 위해
}
}
});
// ref, 객체를 반환하는 함수를 인자로 가짐. (컴포넌트에 노출되어야 하는 속성 및 메소드)
return createPortal(
<dialog ref = {dialog} 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 left.</strong></p>
<form method = 'dialog' onSubmit = {onReset}>
<button>Close</button>
</form>
</dialog>,
document.getElementById('modal')
);
})
export default ResultModal;
시간관련 브라우저의 내장 기능
- setTimeout: 일정 시간 후 한 번 실행.
- setInterval: 일정 간격으로 반복 실행.
- clearTimeout: setTimeout으로 설정된 타이머를 취소.
- clearInterval: setInterval로 설정된 반복 작업을 취소.
최종 TimerChalleng.jsx 코드
import {useRef, useState} from "react";
import ResultModal from "./ResultModal.jsx";
export default function TimerChallenge({title, targetTime}) {
const timer = useRef();
const dialog = useRef();
const [timeRemaining, setTimeRemaining] = useState(
targetTime * 1000
);
// const [timerExpired, setTimerExpired] = useState(false);
// const [timerStarted, setTimerStarted] = useState(false);
const timerIsActive = timeRemaining > 0 && timeRemaining < targetTime * 1000;
if(timeRemaining <= 0 ){
clearInterval(timer.current);
dialog.current.open();
}
function handleReset(){
setTimeRemaining(targetTime*1000);
}
function handleStart() {
timer.current = setInterval(() => {
setTimeRemaining(prevTimeRemaining => prevTimeRemaining - 10);
// setTimerExpired(true);
// dialog.current.open(); // dialog의 내장요소
}, 10);
}
function handleStop(){
dialog.current.open();
clearTimeout(timer.current) // 포인터 : timer의 id를 input으로 필요
}
return (
<>
<ResultModal
ref ={dialog}
targetTime = {targetTime}
remainingTime={timeRemaining}
onReset = {handleReset}
/>
<section className="challenge">
<h2>{title}</h2>
<p className="challenge-time">
{targetTime} second{targetTime > 1 ? 's' : ''}
</p>
<p>
<button onClick={timerIsActive ? handleStop : handleStart}>
{timerIsActive ? 'Stop' : 'Start'} Challenge
</button>
</p>
<p className={timerIsActive ? 'active' : undefined}>
{timerIsActive ? "Time is running..." : "Timer inactive"}
</p>
</section>
</>
);
}
dialog를 ESC를 눌러 닫게되면, 버튼클릭과 달리 onReset을 트리거 하지 않는다.
이를 위해서는 onClose속성에 onReset을 바인딩해주어야 한다.
<dialog onClose={onReset}/>
Portals
현재 이전 코드에서 dialog의 html위치를 찾아보면,
timechallenge의 section 이전에 함께 위치해있는 것을 볼 수 있다.
시각적으로는 문제가 없지만, 기술적으로는 옳지 않다.
(body나 div의 바로 밑에 있는 것이 더 바람직)
시각적으로 보고, html구조를 떠올렸을 때 그 구조가 서로 일치하도록...!
=> 이러한 문제들을 Portal을 통해 해결할 수 있다!
컴포넌트에 렌더링될 html코드를 DOM내 다른 곳으로 옮기는 것!
첫 번째 인수 : jsx 코드
두 번째 인수 : html 요소 (index.html에 존재하는)
iindex.html에서 body바로 밑에 <div id='modal'/>을 추가.
그 후 createPortal을 사용
return createPortal(
<dialog ref = {dialog} 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 left.</strong></p>
<form method = 'dialog' onSubmit = {onReset}>
<button>Close</button>
</form>
</dialog>,
document.getElementById('modal')
);
})
JSX 코드 렌더링이 현재 어플에서 사용하고 있는 곳이 아니라,
웹페이지의 다른 곳에 가야하는 모달이나 비슷한 시나리오가 있을 때 포탈을 자주 사용함.
GitHub - codrae/React-The-Complete-Guide
Contribute to codrae/React-The-Complete-Guide development by creating an account on GitHub.
github.com