JiSoo's Devlog

[Udemy React 완벽 가이드] Section 10 본문

Frontend/React

[Udemy React 완벽 가이드] Section 10

지숭숭숭 2024. 1. 4. 11:45

"Side Effects"란?

  • 애플리케이션에서 일어나는 다른 모든 것을 뜻한다
  • 예를 들면 http 리퀘스트를 보내는 것, 브라우저 저장소에 무언가를 저장하는 것, 코드에서 타이머나 간격 설정
  • 즉 일반적인 컴포넌트 함수 밖에서 일어나는 일
  • useEffect 훅 -> 두 개의 매개변수, 두 개의 인수와 같이 호출된다
  • 첫 번째 인수는 함수로 지정된 의존성이 변경된 경우라면 모든 컴포넌트 평가 후에 실행되어야 하는 함수이다
  • 두 번째 인수로는 지정된 의존성을 넣어줘야 하는데 이것은 의존성으로 구성된 배열이다
  • 의존성이 변경될 때마다 첫 번째 함수가 다시 실행된다
  • 첫 번째 함수에 어떤 사이드 이펙트 코드라도 넣을 수 있는데 그러면 그 코드는 우리가 지정한 의존성이 변경된 경우에만 실행되고 컴포넌트가 다시 렌더링 될 때는 실행되지 않는다
useEffect(() => { ... }, [ dependencies ]);
 

 

 

useEffect() 훅 사용하기

  • 브라우저에 내장된 저장 메커니즘은 리액트와 관련 없다
  • 로컬 저장소를 실행할 수 있는데 setItem은 브라우저에서 사용할 수 있는 전역 객체이다
localStorage.setItem('isLoggedIn', '1');
 
  • setItem 안에 아무 식별자를 넣으면 되는데 문자열이기만 하면 된다
  • 두 번째 인자도 문자열이어야 하는데 예를 들어 로그인했다는 의미로 1이나 0을 써도 되고 logged 이런 식으로 문자열을 써도 된다
 
 
키-값쌍이 위에 코드에서 나온 것

 

  • 저장되어 있는지 확인하고 저장되어 있으면 setIsLoggedIn을 true로 설정하려면 아래 코드를 사용하면 되긴 하지만 무한 루프의 가능성 때문에 문제가 되기에 useEffect를 사용해 아래 코드가 언제 실행될지 제어할 수 있게 해야 한다
const storedUserLoggedInInformation = localStorage.getItem("isLoggedIn");

  if (storedUserLoggedInInformation === "1") {
    setIsLoggedIn(true);
  }
 
  • useEffect를 사용하면 함수가 리액트에 의해 실행되기 때문에 모든 컴포넌트 재평가 후에 실행된다
useEffect(() => {
    const storedUserLoggedInInformation = localStorage.getItem("isLoggedIn");

    if (storedUserLoggedInInformation === "1") {
      setIsLoggedIn(true);
    }
  }, []);
 
  • 앱이 시작되면 이 코드가 실행되고 state가 업데이트되고 컴포넌트 함수가 다시 실행되도록 트리거 된다
  • 데이터 가져오기는 사이드이펙트이고 UI와 직접적인 관련이 없다

 

 

useEffect & 종속성

useEffect(() => {
    setFormIsValid(
      enteredEmail.includes("@") && enteredPassword.trim().length > 6
    );
  }, [setFormIsValid, enteredEmail, enteredPassword]);
 
  • setFormIsValid를 실행하지 않아야 하는데 실행하면 그 결과가 의존성으로 추가되기 때문이다
  • 포인터로 추가하면 본질적으로 함수 그 자체를 의존성으로 추가하는 게 된다
  • 이렇게 하면 모든 로그인 컴포넌트 실행 후에 useEffect 함수를 다시 실행하라고 알려준다
  • setFormIsValid, enteredEmail, enteredPassword가 마지막 컴포넌트 렌더링 주기에서 변경된 경우에만 실행하라고 알려준다
  • 셋 중에 하나도 바뀐 게 없으면 이 이펙트 함수는 다시 실행되지 않는다
  • setFormIsValid는 생략해도 되는데 이런 state 업데이트 함수는 기본적으로 리액트에 의해 절대 변경되지 않도록 보장되기 때문이다
  • 일반적으로 특정 데이터, 어떤 state나 프롭이 변경될 때 로직을 다시 실행하기 위해 이 함수를 사용하기도 한다
  • 사이드이펙트는 보통 http 리퀘스트 등인데 그 예로 키 입력을 듣고 입력된 데이터를 저장하는 것도 있고 그에 대한 응답으로 다른 액션을 실행하는 것도 있다
  • 이메일, 비밀번호 필드의 키 입력에 대한 응답으로 해당 폼의 유효성을 확인하고 업데이트하는 것 또한 사이드이펙트이다

 

 

종속성으로 추가할 항목 및 추가하지 않을 항목

  • effect 함수에서 사용하는 모든 것을 종속성으로 추가해야 한다. 즉, 거기에서 사용하는 모든 상태 변수와 함수를 포함한다
  • 맞는 말이지만 예외가 있다
  1. 상태 업데이트 기능을 추가할 필요가 없다(setFormIsValid 사용) - 리액트는 해당 함수가 절대 변경되지 않도록 보장하므로 종속성으로 추가할 필요가 없다
  2. 내장 API 또는 함수를 추가할 필요가 없다. fetch()나 localStorage 같은 것들(브라우저에 내장된 함수 및 기능, 따라서 전역적으로 사용 가능) - 이런 브라우저 API/전역 가능은 리액트 구성 요소 렌더링 주기와 관련이 없으며 변경되지 않는다
  3. 변수나 함수를 추가할 필요가 없다(ex) 별도의 파일에 새 도우미 함수를 만드는 경우) - 이런 함수 또는 변수도 구성 요소 함수 내부에서 생성되지 않으므로 변경해도 구성 요소 함수 내부에서 생성되지 않으므로 변경해도 구성 요소에 영향을 주지 않는다(해당 변수가 변경되는 경우, 또는 그 반대의 경우에도 구성 요소는 재평가되지 않는다)

 

 

useEffect에서 Cleanup 함수 사용하기

  • 일정량의 키 입력을 수집하거나 또는 키 입력 후 일정 시간 동안 일시 중지되는 것을 기다리는 것을 원한다
  • 사용자가 타이핑을 멈추길 기다리고 그 이후에 유효한지 확인하길 원하는데 이것을 디바운싱 기술이라고 한다
  • 사용자 입력을 디바운스(그룹화)하고 싶은 것
setTimeout(()=>{
    setFormIsValid(
      enteredEmail.includes("@") && enteredPassword.trim().length > 6
    );
}, 500);
 
  • 함수 실행하기 전 500밀리초 기다리기
  • 타이머 저장하고 키가 입력되면 지운다. 타이머가 한 번에 하나만 실행되도록 하기 위해
  • 사용자가 계속 입력하면 다른 모든 타이머는 계속 지워진다
  • 따라서 완료되는 타이머는 하나뿐
  • 첫 번째 인수에 반환값을 줄 수 있는데 함수 자체여야 한다 -> 클린업 함수라고도 하는데 클린업 프로세스로써 실행된다
  • useEffect 함수가 실행될 때마다 클린업 함수가 실행된다
  • 클린업 함수는 우리가 이펙트를 특정한 컴포넌트가 DOM에서 마운트 해제될 때마다 실행된다
  • 클린업 함수는 모든 새로운 사이드이펙트 함수가 실행되기 전에 그리고 컴포넌트가 제거되기 전에 실행된다
  • 첫 번째 사이드이펙트 함수가 실행되기 전에는 실행되지 않는다
useEffect(() => {
    const identifier = setTimeout(() => {
      setFormIsValid(
        enteredEmail.includes("@") && enteredPassword.trim().length > 6
      );
    }, 500);

    return () => {
      console.log("CLEANUP");
      clearTimeout(identifier);
    };
  }, [enteredEmail, enteredPassword]);
 
  • return 값에 clearTimeout을 주면 클린업 함수가 실행될 때마다 클린업 함수가 실행되기 전에 설정된 타이머를 지운다
  • 새로운 타이머를 설정하기 전에 마지막 타이머를 지우는 것

 

 

useEffect 요약

  • 이펙트 함수는 컴포넌트가 처음 마운트 된 때를 포함해 모든 컴포넌트 렌더링 주기 후에 실행
  • 의존성이 추가되면 컴포넌트가 재평가될 때마다 재실행된다
  • 클린업 함수는 state 함수가 전체적으로 실행되기 전에 실행되지만 처음 실행되기 전에는 실행되지 않는다

 

 

useReducer 및 Reducers 일반 소개

  • 강력한 state 관리가 필요한 경우 useState를 대체할 수 있다
  • 다른 state를 기반으로 하는 state를 업데이트하면 그 경우에는 하나의 state로 병합하는 것도 좋다

 

 

useReducer() 훅 사용

  • useReducer도 항상 두 개의 값이 있는 배열을 반환하기 때문에 배열 디스트럭처링 사용 가능
const [state, dispatchFn] = useReducer(reducerFn, initialState, initFn);
 
  • 반환되는 두 값중 하나는 최신 state 스냅샷이다 -> state 관리 메커니즘이기 때문에
  • 또 하나는 업데이트할 수 있게 해주는 함수도 얻게 된다
  • 리듀서 함수는 최신 state 스냅샷을 자동으로 가져오는 함수이고 이 함수는 디스패치된 액션을 가져온다
  • 리액트는 새 액션이 디스패치될 때마다 이 리듀서 함수를 호출한다
  • 리듀서 함수는 새로운 업데이트된 state를 반환한다
  • 리듀서 함수는 컴포넌트 함수 바깥에 만드는데 리듀서 함수 내부에서는 컴포넌트 함수 내부에서 만들어진 어떤 데이터도 필요하지 않기 때문이다
  • 리듀서 함수와 컴포넌트 함수 내부에서 정의된 그 어떤 것과도 상호작용할 필요가 없다

 

import React, { useState, useEffect, useReducer } from "react";

import Card from "../UI/Card/Card";
import classes from "./Login.module.css";
import Button from "../UI/Button/Button";

const emailReducer = (state, action) => {
  if (action.type === "USER_INPUT") {
    return { value: action.val, isValid: action.val.includes("@") };
  }
  if (action.type === "INPUT_BLUR") {
    return { value: state.value, isValid: state.value.includes("@") };
  }
  return { value: "", isValid: false };
};

const Login = (props) => {
  const [enteredPassword, setEnteredPassword] = useState("");
  const [passwordIsValid, setPasswordIsValid] = useState();
  const [formIsValid, setFormIsValid] = useState(false);

  const [emailState, dispatchEmail] = useReducer(emailReducer, {
    value: "",
    isValid: null,
  });

  useEffect(() => {
    console.log("EFFECT RUNNING");

    return () => {
      console.log("EFFECT CLEANUP");
    };
  }, [enteredPassword]);

  const emailChangeHandler = (event) => {
    dispatchEmail({ type: "USER_INPUT", val: event.target.value });

    setFormIsValid(
      event.target.value.includes("@") && enteredPassword.trim().length > 6
    );
  };

  const passwordChangeHandler = (event) => {
    setEnteredPassword(event.target.value);
    setFormIsValid(emailState.isValid && event.target.value.trim().length > 6);
  };

  const validateEmailHandler = () => {
    dispatchEmail({ type: "INPUT_BLUR" });
  };

  const validatePasswordHandler = () => {
    setPasswordIsValid(enteredPassword.trim().length > 6);
  };

  const submitHandler = (event) => {
    event.preventDefault();
    props.onLogin(emailState.value, enteredPassword);
  };

  return (
    <Card className={classes.login}>
      <form onSubmit={submitHandler}>
        <div
          className={`${classes.control} ${
            emailState.isValid === false ? classes.invalid : ""
          }`}
        >
          <label htmlFor="email">E-Mail</label>
          <input
            type="email"
            id="email"
            value={emailState.value}
            onChange={emailChangeHandler}
            onBlur={validateEmailHandler}
          />
        </div>
        <div
          className={`${classes.control} ${
            passwordIsValid === false ? classes.invalid : ""
          }`}
        >
          <label htmlFor="password">Password</label>
          <input
            type="password"
            id="password"
            value={enteredPassword}
            onChange={passwordChangeHandler}
            onBlur={validatePasswordHandler}
          />
        </div>
        <div className={classes.actions}>
          <Button type="submit" className={classes.btn} disabled={!formIsValid}>
            Login
          </Button>
        </div>
      </form>
    </Card>
  );
};

export default Login;
 

 

 

useReducer & useEffect

  • 객체디스트럭처링은 객체의 특정 속성 추출
import React, { useState, useEffect, useReducer } from "react";

import Card from "../UI/Card/Card";
import classes from "./Login.module.css";
import Button from "../UI/Button/Button";

const emailReducer = (state, action) => {
  if (action.type === "USER_INPUT") {
    return { value: action.val, isValid: action.val.includes("@") };
  }
  if (action.type === "INPUT_BLUR") {
    return { value: state.value, isValid: state.value.includes("@") };
  }
  return { value: "", isValid: false };
};

const passwordReducer = (state, action) => {
  if (action.type === "USER_INPUT") {
    return { value: action.val, isValid: action.val.trim().length > 6 };
  }
  if (action.type === "INPUT_BLUR") {
    return { value: state.value, isValid: state.value.trim().length > 6 };
  }
  return { value: "", isValid: false };
};

const Login = (props) => {
  // const [enteredEmail, setEnteredEmail] = useState("");
  // const [emailIsValid, setEmailIsValid] = useState();
  // const [enteredPassword, setEnteredPassword] = useState("");
  // const [passwordIsValid, setPasswordIsValid] = useState();
  const [formIsValid, setFormIsValid] = useState(false);

  const [emailState, dispatchEmail] = useReducer(emailReducer, {
    value: "",
    isValid: null,
  });

  const [passwordState, dispatchPassword] = useReducer(passwordReducer, {
    value: "",
    isValid: null,
  });

  useEffect(() => {
    console.log("EFFECT RUNNING");

    return () => {
      console.log("EFFECT CLEANUP");
    };
  }, []);

  const { isValid: emailIsValid } = emailState;
  const { isValid: passwordIsValid } = passwordState;

  useEffect(() => {
    const identifier = setTimeout(() => {
      console.log("Checking form validity!");
      setFormIsValid(emailIsValid && passwordIsValid);
    }, 500);

    return () => {
      console.log("CLEANUP");
      clearTimeout(identifier);
    };
  }, [emailIsValid, passwordIsValid]);

  const emailChangeHandler = (event) => {
    dispatchEmail({ type: "USER_INPUT", val: event.target.value });

    // setFormIsValid(
    //   event.target.value.includes("@") && passwordState.isValid
    // );
  };

  const passwordChangeHandler = (event) => {
    dispatchPassword({ type: "USER_INPUT", val: event.target.value });
    // setFormIsValid(emailState.isValid && event.target.value.trim().length > 6);
  };

  const validateEmailHandler = () => {
    dispatchEmail({ type: "INPUT_BLUR" });
  };

  const validatePasswordHandler = () => {
    dispatchPassword({ type: "INPUT_BLUR" });
  };

  const submitHandler = (event) => {
    event.preventDefault();
    props.onLogin(emailState.value, passwordState.value);
  };

  return (
    <Card className={classes.login}>
      <form onSubmit={submitHandler}>
        <div
          className={`${classes.control} ${
            emailState.isValid === false ? classes.invalid : ""
          }`}
        >
          <label htmlFor="email">E-Mail</label>
          <input
            type="email"
            id="email"
            value={emailState.value}
            onChange={emailChangeHandler}
            onBlur={validateEmailHandler}
          />
        </div>
        <div
          className={`${classes.control} ${
            passwordState.isValid === false ? classes.invalid : ""
          }`}
        >
          <label htmlFor="password">Password</label>
          <input
            type="password"
            id="password"
            value={passwordState.value}
            onChange={passwordChangeHandler}
            onBlur={validatePasswordHandler}
          />
        </div>
        <div className={classes.actions}>
          <Button type="submit" className={classes.btn} disabled={!formIsValid}>
            Login
          </Button>
        </div>
      </form>
    </Card>
  );
};

export default Login;
 

 

 

중첩 속성을 useEffect에 종속성으로 추가하기

  • useEffect()에 객체 속성을 종속성으로 추가하기 위해 destructuring을 사용했는데 이건 매우 일반적인 패턴 및 접근 방식
  • 핵심은 destructuring을 사용한다는 것이 아니라 특정 속성을 종속성으로 전달한다는 것이다

 

 

State 관리를 위한 useReducer 대 useState

  • useReducer은 useState를 사용하면 너무 번거로운 경우 즉, 너무 많은 일을 처리해야 하는 경우에 사용한다
  • 관련 state 스냅샷들이 서로 독립적이고 같이 업데이트가 잘 안된다면 useReducer을 사용한다
  • useState는 주요 state 관리 도구이다
  • 보통 useState로 시작하는데 대부분의 경우에는 그것만 있으면 된다
  • 개별 state 및 데이터들을 다루기에 적합하고 간단한 state에 적합하다
  • state 업데이트가 쉽고 몇 종류 안 되는 경우에 적합하다
  • 만약 state로서의 객체가 있는 경우 또는 복잡한 state가 있다면 useReducer를 고려할 수 있다
  • 일반적으로 useReducer는 리듀서 함수를 쓸 수 있기 때문에 더 강력하다
  • 복잡한 state 업데이트 로직을 포함하는 리듀서 함수를 쓸 수 있다
  • 또한 그 복잡할 수도 있는 로직을 컴포넌트 함수 body에서 별도의 리듀서 함수로 이동시킬 수도 있다
  • 특히 useReducer를 고려해야 하는 경우는 연관된 state 조각들로 구성된 state 관련 데이터를 다루는 경우이다
  • 예를 들어 폼 인풋 state가 있는 경우에는 useReducer가 도움이 될 수 있다
  • 두 개의 서로 다른 값을 전환하기만 하는 단순한 state가 있는 경우라면 useReducer는 과도할 수 있다
  • useReducer를 사용해 폼 state를 전체적으로 관리할 수도 있다

 

 

리액트 Context(Context API)소개

  • 프롭을 통해 많은 컴포넌트를 거쳐 많은 데이터를 전달할 때 문제가 발생한다
  • 데이터를 컴포넌트를 통해 다른 컴포넌트에 전달하기 위해 프롭 체인이 만들어지기 쉽다

 

 

리액트 컨텍스트 API 사용

  • 리액트 컨텍스트는 리액트 내부적으로 state를 관리할 수 있도록 해주는 것
  • 앱의 어떤 컴포넌트에서라도 직접 변경해 프롭체인을 구축하지 않아도 앱의 다른 컴포넌트에 직접 전달할 수 있게 해준다
  • createContext는 기본 컨텍스트를 만든다
  • 컨텍스트는 앱이나 빈 state의 컴포넌트일 뿐이기 때문에 그 state가 어떠해야 하는지는 우리가 정해야 하는데 대부분의 경우에는 객체를 사용한다
  • createContext를 통해 얻는 것은 컴포넌트가 되거나 컴포넌트를 포함하는 객체가 될 것이다
  • 앱에서 컨텍스트를 사용하려면 먼저 공급해야 하고 소비해야 한다
  • 공급한다는 것은 JSX 코드로 감싸는 것을 의미한다
import React from "react";

const AuthContext = React.createContext({
  isLoggedIn: false,
});

export default AuthContext;
 
 

 

 

useContext 훅으로 컨텍스트에 탭핑(tapping)하기

import React, { useContext } from "react";

import classes from "./Navigation.module.css";
import AuthContext from "../../store/auth-context";

const Navigation = (props) => {
  const ctx = useContext(AuthContext);

  return (
    <nav className={classes.nav}>
      <ul>
        {ctx.isLoggedIn && (
          <li>
            <a href="/">Users</a>
          </li>
        )}
        {ctx.isLoggedIn && (
          <li>
            <a href="/">Admin</a>
          </li>
        )}
        {ctx.isLoggedIn && (
          <li>
            <button onClick={props.onLogout}>Logout</button>
          </li>
        )}
      </ul>
    </nav>
  );
};

export default Navigation;
 

 

 

컨텍스트를 동적으로 만들기

  • 컴포넌트뿐만 아니라 함수에도 데이터를 전달하도록 동적 컨텍스트를 설정할 수 있다
  • 예를 들어 내비게이션처럼 매우 특정적인 일을 하는 컴포넌트에서 항상 사용자를 로그아웃시키는 버튼 같은 경우에 컴포넌트로 전달하는 경우에만 컨텍스트를 사용하는 게 좋다
  • 컨텍스트를 사용하면 더 간결한 코드를 작성할 수 있고 앱 전체 state 관리가 조금 더 쉬워진다

 

 

사용자 정의 컨텍스트 제공자 구성요소 빌드 및 사용

auth-context.js

import React, { useState, useEffect } from "react";

const AuthContext = React.createContext({
  isLoggedIn: false,
  onLogout: () => {},
  onLogin: (email, password) => {},
});

export const AuthContextProvider = (props) => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  useEffect(() => {
    const storedUserLoggedInInformation = localStorage.getItem("isLoggedIn");

    if (storedUserLoggedInInformation === "1") {
      setIsLoggedIn(true);
    }
  }, []);

  const logoutHandler = () => {
    localStorage.removeItem("isLoggedIn");
    setIsLoggedIn(false);
  };

  const loginHandler = () => {
    localStorage.setItem("isLoggedIn", "1");
    setIsLoggedIn(true);
  };

  return (
    <AuthContext.Provider
      value={{
        isLoggedIn: isLoggedIn,
        onLogout: logoutHandler,
        onLogin: loginHandler,
      }}
    >
      {props.children}
    </AuthContext.Provider>
  );
};

export default AuthContext;
 

 

 

리액트 컨텍스트 제한

  • 앱 전체 또는 컴포넌트 전체 state에는 컨텍스트가 적합할 수 있다
  • 하지만 컴포넌트 구성을 대체할 수는 없다
  • state가 1초에도 여러 번 바뀌는 경우라면 리액트 컨텍스트는 이에 최적화되어 있지 않다
  • state가 자주 변경되는 경우에 더 적합한 도구는 리덕스

 

 

"Hooks의 규칙" 배우기

  • 리액트 훅은 단순히 use로 시작하는 모든 함수이다
  • 리액트 훅은 리액트 컴포넌트 함수 또는 사용자 정의 훅에서만 호출해야 한다
  • 리액트 훅은 리액트 컴포넌트 함수 또는 사용자 정의 훅 함수의 최상위 수준에서만 호출해야 한다
  • 중첩 함수나 block문, if문에서 호출하면 안 된다
  • 항상 참조하는 모든 항목을 의존성으로 useEffect 내부에 추가해야 한다

 

 

입력 컴포넌트 리팩토링

Input.js

import React from "react";

import classes from "./Input.module.css";

const Input = (props) => {
  return (
    <div
      className={`${classes.control} ${
        props.isValid === false ? classes.invalid : ""
      }`}
    >
      <label htmlFor={props.id}>{props.label}</label>
      <input
        type={props.type}
        id={props.id}
        value={props.value}
        onChange={props.onChange}
        onBlur={props.onBlur}
      />
    </div>
  );
};

export default Input;
 

 

 

Forward Refs에 대해 알아보기

  • 컴포넌트 내부에서 함수를 호출하는 방식은 일반적인 패턴이 아니기 때문에 자주 할 필요도 없고 자주 해서도 안 된다.

 

Input.js

import React, { useRef, useImperativeHandle } from "react";

import classes from "./Input.module.css";

const Input = React.forwardRef((props, ref) => {
  const inputRef = useRef();

  const activate = () => {
    inputRef.current.focus();
  };

  useImperativeHandle(ref, () => {
    return {
      focus: activate,
    };
  });

  return (
    <div
      className={`${classes.control} ${
        props.isValid === false ? classes.invalid : ""
      }`}
    >
      <label htmlFor={props.id}>{props.label}</label>
      <input
        ref={inputRef}
        type={props.type}
        id={props.id}
        value={props.value}
        onChange={props.onChange}
        onBlur={props.onBlur}
      />
    </div>
  );
});

export default Input;
 

Login.js

import React, {
  useState,
  useEffect,
  useReducer,
  useContext,
  useRef,
} from "react";

import Card from "../UI/Card/Card";
import Button from "../UI/Button/Button";
import AuthContext from "../../store/auth-context";
import Input from "../UI/Input/Input";
import classes from "./Login.module.css";

const emailReducer = (state, action) => {
  if (action.type === "USER_INPUT") {
    return { value: action.val, isValid: action.val.includes("@") };
  }
  if (action.type === "INPUT_BLUR") {
    return { value: state.value, isValid: state.value.includes("@") };
  }
  return { value: "", isValid: false };
};

const passwordReducer = (state, action) => {
  if (action.type === "USER_INPUT") {
    return { value: action.val, isValid: action.val.trim().length > 6 };
  }
  if (action.type === "INPUT_BLUR") {
    return { value: state.value, isValid: state.value.trim().length > 6 };
  }
  return { value: "", isValid: false };
};

const Login = (props) => {
  // const [enteredEmail, setEnteredEmail] = useState('');
  // const [emailIsValid, setEmailIsValid] = useState();
  // const [enteredPassword, setEnteredPassword] = useState('');
  // const [passwordIsValid, setPasswordIsValid] = useState();
  const [formIsValid, setFormIsValid] = useState(false);

  const [emailState, dispatchEmail] = useReducer(emailReducer, {
    value: "",
    isValid: null,
  });
  const [passwordState, dispatchPassword] = useReducer(passwordReducer, {
    value: "",
    isValid: null,
  });

  const authCtx = useContext(AuthContext);

  const emailInputRef = useRef();
  const passwordInputRef = useRef();

  useEffect(() => {
    console.log("EFFECT RUNNING");

    return () => {
      console.log("EFFECT CLEANUP");
    };
  }, []);

  const { isValid: emailIsValid } = emailState;
  const { isValid: passwordIsValid } = passwordState;

  useEffect(() => {
    const identifier = setTimeout(() => {
      console.log("Checking form validity!");
      setFormIsValid(emailIsValid && passwordIsValid);
    }, 500);

    return () => {
      console.log("CLEANUP");
      clearTimeout(identifier);
    };
  }, [emailIsValid, passwordIsValid]);

  const emailChangeHandler = (event) => {
    dispatchEmail({ type: "USER_INPUT", val: event.target.value });

    // setFormIsValid(
    //   event.target.value.includes('@') && passwordState.isValid
    // );
  };

  const passwordChangeHandler = (event) => {
    dispatchPassword({ type: "USER_INPUT", val: event.target.value });

    // setFormIsValid(emailState.isValid && event.target.value.trim().length > 6);
  };

  const validateEmailHandler = () => {
    dispatchEmail({ type: "INPUT_BLUR" });
  };

  const validatePasswordHandler = () => {
    dispatchPassword({ type: "INPUT_BLUR" });
  };

  const submitHandler = (event) => {
    event.preventDefault();
    if (formIsValid) {
      authCtx.onLogin(emailState.value, passwordState.value);
    } else if (!emailIsValid) {
      emailInputRef.current.focus();
    } else {
      passwordInputRef.current.focus();
    }
  };

  return (
    <Card className={classes.login}>
      <form onSubmit={submitHandler}>
        <Input
          ref={emailInputRef}
          id="email"
          label="E-Mail"
          type="email"
          isValid={emailIsValid}
          value={emailState.value}
          onChange={emailChangeHandler}
          onBlur={validateEmailHandler}
        />
        <Input
          ref={passwordInputRef}
          id="password"
          label="Password"
          type="password"
          isValid={passwordIsValid}
          value={passwordState.value}
          onChange={passwordChangeHandler}
          onBlur={validatePasswordHandler}
        />
        <div className={classes.actions}>
          <Button type="submit" className={classes.btn}>
            Login
          </Button>
        </div>
      </form>
    </Card>
  );
};

export default Login;
 

 

728x90