useRef와 forwardRef, useImperativeHandle
useRef
-
reference의 줄임말로, 무엇이든지 참조해주겠다!
-
onChange와 같은 무거운 Event Handler를 사용하지 않아도 됨 (DOM을 외부에 공유하는 것도 가능)
-
useState는 메모리 주소가 바뀌면 VD에서 무조건 re-render가 발생함 (→ 무거워짐)
-
useRef는 ref 속에 있는 current 값의 주소가 바뀜(→ ref 자체의 주소는 바뀌지 않음)
⇒ re-rendering 없이 상태 값이 유지됨
forwardRef
- useRef로 컴포넌트 외부 DOM 접근하기
props
와ref
와 함께 이 함수를 호출- 18.3부터는 prop으로 전달 가능!
- useImperativeHandle과 함께 쓰여 부모가 자식 컴포넌트를 제어할 수 있음
- 18.3~이어도 useImperativeHandle을 사용할 때에는, ref,
useImperativeHandle
- ref로 노출되는 핸들을 사용자가 직접 정의할 수 있게 해주는 React 훅
useImperativeHandle(ref, createHandle, dependencies?)
- ref : forwardRef 렌더링 함수에서 두 번째 인자로 받은 ref
- createHandle : 인자가 없고 노출하려는 ref 핸들을 반환하는 함수
부모 컴포넌트에 커스텀 ref 핸들 노출
-
기본적으로 컴포넌트는 자식 컴포넌트의 DOM 노드를 부모 컴포넌트에 접근하지 않도록 함
-
부모 컴포넌트가 자식 컴포넌트의 DOM 노드에 접근하려면 **
forwardRef
**를 사용자식 컴포넌트import { forwardRef } from 'react'; const MyInput = forwardRef(function MyInput(props, ref) { return <input {...props} ref={ref} />; });
-
useImperativeHandle
을 사용해 부모 컴포넌트에서 호출할 메서드만 노출부모 컴포넌트import { forwardRef, useRef, useImperativeHandle } from 'react'; const MyInput = forwardRef(function MyInput(props, ref) { const inputRef = useRef(null); useImperativeHandle(ref, () => { return { // ... 메서드를 여기에 입력하세요 ... focus() { inputRef.current.focus(); }, scrollIntoView() { inputRef.current.scrollIntoView(); }, }; }, []); return <input {...props} ref={inputRef} />; });
-
이후, 부모 컴포넌트에서는 useRef를 사용해 자식 컴포넌트에 대한 ref를 가져올 수 있음
⇒
focus
및scrollIntoView
메서드 호출 가능부모 컴포넌트import { useRef } from 'react'; import MyInput from './MyInput.js'; export default function Form() { const ref = useRef(null); function handleClick() { ref.current.focus(); // 이 작업은 DOM 노드가 노출되지 않으므로 작동하지 않습니다. // ref.current.style.opacity = 0.5; } return ( <form> <MyInput placeholder="Enter your name" ref={ref} /> <button type="button" onClick={handleClick}> Edit </button> </form> ); }
자식 컴포넌트import { forwardRef, useRef, useImperativeHandle } from 'react'; const MyInput = forwardRef(function MyInput(props, ref) { const inputRef = useRef(null); useImperativeHandle(ref, () => { return { focus() { inputRef.current.focus(); }, scrollIntoView() { inputRef.current.scrollIntoView(); }, }; }, []); return <input {...props} ref={inputRef} />; }); export default MyInput;
-
사용 예시
최상위(조상) 컴포넌트import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import './App.css'; import Hello from './components/Hello'; import My from './components/My'; import { LoginImperativeHandler } from './components/Login'; export type Session = { loginUser?: { id: number; name: string } | null; cart: { id: number; name: string; price: number }[]; }; const SampleSession: Session = { // loginUser: { id: 1, name: 'Hong' }, cart: [ { id: 100, name: '라면', price: 3000 }, { id: 101, name: '컵라면', price: 2000 }, { id: 200, name: '파', price: 5000 }, ], }; type ChildHandler = { f: (s: string) => void; }; const ChildComponent = forwardRef(({ age }: { age: number }, ref) => { const handler = { f(s: string) { console.log('s=', s); }, }; useImperativeHandle(ref, () => handler); return <>age: {age}</>; }); function App() { const [count, setCount] = useState(0); const [session, setSession] = useState<Session>(SampleSession); const addBtnRef = useRef<HTMLButtonElement>(null); const childRef = useRef<ChildHandler>(null); const loginFnRef = useRef<LoginImperativeHandler>(null); const logout = () => { setSession({ ...session, loginUser: null }); }; const login = (id: number, name: string) => { if (!name) { alert('no name'); loginFnRef.current?.focusName(); return; } setSession({ ...session, loginUser: { id: id, name: name } }); }; const removeCartItem = (itemId: number) => { setSession({ ...session, cart: session.cart.filter(({ id }) => id !== itemId), }); }; const addCartItem = (name: string, price: number) => { const id = Math.max(...session.cart.map((item) => item.id), 0) + 1; setSession({ ...session, cart: [...session.cart, { id, name, price }], }); }; return ( <> <ChildComponent ref={childRef} age={count} /> <button onClick={() => childRef.current?.f('XXX')} className='btn'> ChildFn </button> <Hello name='Jade' age={33}> CCC </Hello> <My session={session} logout={logout} login={login} removeCartItem={removeCartItem} addCartItem={addCartItem} addBtnRef={addBtnRef} loginFnRef={loginFnRef} /> <div className='card'> <button onClick={() => setCount((count) => count + 1)}> count is {count} </button> </div> </> ); } export default App;
부모 컴포넌트import { useRef, useState } from 'react'; import { Session } from '../App'; import Login, { LoginImperativeHandler } from './Login'; import Profile from './Profile'; import { FaPlus, FaRedo } from 'react-icons/fa'; type Props = { session: Session; logout: () => void; login: (id: number, name: string) => void; removeCartItem: (itemId: number) => void; addCartItem: (name: string, price: number) => void; addBtnRef: React.RefObject<HTMLButtonElement>; loginFnRef: React.RefObject<LoginImperativeHandler>; }; export default function My({ session: { loginUser, cart }, logout, login, removeCartItem, addCartItem, addBtnRef, loginFnRef, }: Props) { const [isEditing, setIsEditing] = useState(false); const nameRef = useRef<HTMLInputElement>(null); const priceRef = useRef<HTMLInputElement>(null); const addItem = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const name = nameRef.current?.value; const price = priceRef.current?.value; if (!name) { alert('Input the name, plz'); focus(nameRef); return; } if (!price) { alert('Input the price, plz'); focus(priceRef); return; } addCartItem(name, +price); nameRef.current.value = ''; priceRef.current.value = '0'; setIsEditing(false); }; const focus = (ref: React.RefObject<HTMLInputElement>) => { if (ref.current) ref.current.focus(); }; return ( <div className='border-2 border-red-300 p-1'> <ul> {cart.map(({ id, name, price }) => ( <li key={id}> <small className='mx-2 text-gray-300'>{id}.</small> {name} <span className='text-gray-500'>({price})</span> <button onClick={() => removeCartItem(id)} className='btn-danger'> DEL </button> </li> ))} </ul> {isEditing ? ( <form onSubmit={addItem} className='mb-3 flex gap-2 border p-2'> <input type='text' ref={nameRef} placeholder='name...' className='border border-slate-500 focus:border-blue-300' /> <input type='number' ref={priceRef} placeholder='price...' /> <button onClick={() => setIsEditing(false)} className='btn'> <FaRedo /> </button> <button ref={addBtnRef} type='submit' className='btn-primary'> <FaPlus /> </button> </form> ) : ( <button onClick={() => setIsEditing(true)} className='btn mb-3'> +추가 </button> )} <hr /> {loginUser ? ( <Profile loginUser={loginUser} logout={logout} /> ) : ( <Login login={login} ref={loginFnRef} /> )} </div> ); }
자식 컴포넌트import { forwardRef, useImperativeHandle, useRef } from 'react'; type Props = { login: (id: number, name: string) => void }; export type LoginImperativeHandler = { focusName: () => void; }; const Login = forwardRef(({ login }: Props, ref) => { const userIdRef = useRef<HTMLInputElement>(null); const userNameRef = useRef<HTMLInputElement>(null); useImperativeHandle(ref, () => ({ focusName() { userNameRef.current?.focus(); }, })); const submitHandle = (evt: React.FormEvent<HTMLFormElement>) => { evt.preventDefault(); const id = userIdRef.current?.value ?? 0; const name = userNameRef.current?.value ?? ''; login(+id, name); }; console.log('@@@Login'); return ( <form onSubmit={submitHandle}> <div> Login Name: <input type='text' ref={userNameRef} /> </div> <div> Login ID(숫자):{' '} <input type='number' defaultValue={100} ref={userIdRef} /> </div> <button type='submit' className='btn-primary'> Login </button> </form> ); }); export default Login;
!! 조상 → 부모 → 자식 !!
으로 값을 넘기는게 거슬려야 함!
→ 이후 useContext로 코드 리팩터링 예정