Study
react
Useref and Forwardref and Useimperativehandle

useRef와 forwardRef, useImperativeHandle

useRef

  • reference의 줄임말로, 무엇이든지 참조해주겠다!

  • onChange와 같은 무거운 Event Handler를 사용하지 않아도 됨 (DOM을 외부에 공유하는 것도 가능)

  • useState는 메모리 주소가 바뀌면 VD에서 무조건 re-render가 발생함 (→ 무거워짐)

  • useRef는 ref 속에 있는 current 값의 주소가 바뀜(→ ref 자체의 주소는 바뀌지 않음)

    re-rendering 없이 상태 값이 유지됨

forwardRef

  • useRef로 컴포넌트 외부 DOM 접근하기
  • propsref와 함께 이 함수를 호출
    • 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를 가져올 수 있음

    focusscrollIntoView 메서드 호출 가능

    부모 컴포넌트
    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로 코드 리팩터링 예정