Effects, Reducers & Context, Hooks의 규칙

김민경·2023년 2월 3일
0

FE(WEB) - React

목록 보기
7/13

🎈useEffect()

: Working with (Side) Effects

What is an "Effect"(or a "Side Effect")?

  • if we send a http request directly to the component,
    there would be a http request whenever the function(component) runs
  • when the state changes in response to the http request,
    it would create an infinite loop
    (http request -> state changes -> function(component) runs -> ...)

=> so we should not directly code side effects to the component

useEffect()

  • in this example, we want to check if the user is loggined
    (in real world, we send a http request to the server, get an access token)
  • we will store the loggined-state in the browser storage, such as cookies or local Storage
  • when we directly manage it in the component, it triggers infinite-loop
    (localStorage.getItem() -> state changes(setLoggined(true) -> function(component) runs -> ...)

=> so we have to use useEffect()
=> it is executed after every component is re-evaluated
, only when the dependencies are changed

(having dependencies for [], when the component is evaluated
, it is regarded as dependencies are changed)

function App() {
	const [isLoggedIn, setIsLoggedIn] = useState(false);

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

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

  const loginHandler = (email, password) => {
    // We should of course check email and password
    // But it's just a dummy/ demo anyways
    localStorage.setItem('isLoggedIn', '1');
    setIsLoggedIn(true);
  };
  
  const logoutHandler = () => {
    localStorage.removeItem('isLoggedIn');
    setIsLoggedIn(false);
  };

  return (
    <React.Fragment>
      <MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
      <main>
        {!isLoggedIn && <Login onLogin={loginHandler} />}
        {isLoggedIn && <Home onLogout={logoutHandler} />}
      </main>
    </React.Fragment>
  );

}

(function(component) executes(renders) -> useEffect -> setIsLoggedIn(true) -> function(component) re-renders)
-> since the dependency is not changed, useEffect isn't executed again
=> therefore the useEffect code runs only once

useEffect() with dependencies

an input example (whether the form is valid or invalid)

useEffect(()=> {
	setFormIsValid(
    	enteredEmail.includes('@') && enteredPassword.trim().length > 6
    )
}, [enteredEmail, enteredPassword])

what should not be regarded as dependencies

  • 상태 업데이트 기능 (setSomething~)
  • "내장" API 또는 함수 (fetch(), localStorage)
    -> (브라우저에 내장된 함수 및 기능, 따라서 전역적으로 사용 가능): 이러한 브라우저 API/전역 기능은 React 구성 요소 렌더링 주기와 관련이 없으며 변경되지 않습니다.
  • 구성 요소 외부에서 정의한 변수나 함수
    -> (예: 별도의 파일에 helperFunction를 만드는 경우): 이러한 함수 또는 변수도 구성 요소 함수 내부에서 생성되지 않으므로 변경해도 구성 요소에 영향을 주지 않습니다

what should be regarded as dependencies

  • effect 함수에서 사용하는 모든 "것들"을 추가해야 합니다.
  • 구성 요소(또는 일부 상위 구성 요소)가 다시 렌더링 되어 이러한 "것들"이 변경될 수 있는 경우
  • 그렇기 때문에 컴포넌트 함수에 정의된 변수나 상태, 컴포넌트 함수에 정의된 props 또는 함수는 종속성으로 추가되어야 합니다!

(Ex)

import { useEffect, useState } from 'react';
 
let myTimer;
 
const MyComponent = (props) => {
  const [⭐timerIsActive, setTimerIsActive] = useState(false);
 
  const { ⭐timerDuration } = props; 
  // using destructuring to pull out specific props values
 
  useEffect(() => {
    if (!timerIsActive) {
      setTimerIsActive(true);
      myTimer = setTimeout(() => {
        setTimerIsActive(false);
      }, timerDuration);
    }
  }, [timerIsActive, timerDuration]);
};

// (종속성으로 추가 O)
// timerIsActive : 구성 요소가 변경될 때 변경될 수 있는 구성 요소 상태
// (예: 상태가 업데이트되었기 때문)
// timerDuration : 해당 구성 요소의 prop 값이기 때문 
// - 따라서 상위 구성 요소가 해당 값을 변경하면 변경될 수 있습니다
// (이 MyComponent 구성 요소도 다시 렌더링되도록 함)

// (종속성으로 추가 X)
// setTimerIsActive : 상태 업데이트 기능
// myTimer : 구성 요소 내부 변수가 아니기 때문이죠. 
// (즉, 어떤 상태나 prop 값이 아님) - 구성 요소 외부에서 정의되고 이를 변경합니다(어디에서든). 
// 구성 요소가 다시 평가되도록 하지 않습니다.
// setTimeout : 내장 API이기 때문입니다. (브라우저에 내장) 
//- React 및 구성 요소와 독립적이며 변경되지 않습니다.

cleanup function

cleanup function에 대한 더 자세한 설명

useEffect(()=> {
	// console.log('Checking form validity')
    // whenever we change the input state (enteredEmail, enteredPassword) 
    // console.log() executes
    // this might be a problem when we want to put a http request code
    // because there might be requests whenever the input state changes 
    // (triggering unnecessary network traffics)
	setFormIsValid(
    	enteredEmail.includes('@') && enteredPassword.trim().length > 6
    )
}, [enteredEmail, enteredPassword])
useEffect(()=> {
	// ⭐debouncing(그룹화)
    // 이벤트를 그룹화하여 특정시간이 지난 후 하나의 이벤트만 발생하도록 하는 기술
	// 즉, 순차적 호출을 하나의 그룹으로 "그룹화"할 수 있습니다
    // when there is a pause during typing, we want to execute the code
    
    const identifier = setTimeout(() => {
    	setFormIsValid(
    		enteredEmail.includes('@') && enteredPassword.trim().length > 6
    	)
    }
    , 500);
    // 이 코드만 가지고는 효과가 없다
    // 모든 키 입력에 대해서 타이머를 지정했기 때문
    
    // ⭐clean-up function
    // we actually save the timer,
    // and for the next keystroke, we clear it
    // so that we only have one ongoing timer at a time

    // as long as the user keeps typing, we always clear all other timers
    // therefore we only have one timer that completes after 500 milliseconds
    // (delay the user has to issue a new keystroke to clear this timer)
    
    return () => { 
    	console.log('clean-up');
        clearTimeout(identifier);
    } 
    // the clean-up the function executes 
    // ✨before setTimeout executes a next time(when the dependencies' state change)
    // ✨, and before the component unmounts
    // (it will not execute the first time before setTimeout executes)
    // so when we type,
    // console.log('clean-up') executes many times according to the keystroke
    // but setFormisValid() executes only once between the interval
}, [enteredEmail, enteredPassword])

🎈useReducer()

: Managing more Complex State with Reducers

When it is needed?
=> if you update a state which depends on another state
=> then merging to one state could be a good idea

const [enteredEmail, setEnteredEmail] = useState('');
const [emailIsValid, setEmailIsValid] = useState();
// ❗ belongs togeteher, why don't we manage it together?
const [enteredPassword, setEnteredPassword] = useState('');
const [passwordIsValid, setPasswordIsValid] = useState();
// ❗ belongs togeteher, why don't we manage it together?
const [formIsValid, setFormIsValid] = useState(false);

const emailChangeHandler = (e) => {
	setEnteredEmail(e.target.value);
    
    setFormIsValid(
    	e.target.value.includes('@;) && enteredPassword.trim().length > 6
    );
    // strictly, we have to use the function form
    // when updating state based on some older state
    // ❗ but here we depend on two different prev. states
    // -> has a possibility of bugs
};

const passwordChangeHandler = (e) => {
	setEnteredPassword(e.target.value);
    
    setFormIsValid(
    	enteredEmail.includes('@;) && e.target.value.trim().length > 6
    );
    // strictly, we have to use the function form
    // when updating state based on some older state
    // ❗ but here we depend on two different prev. states
    // -> has a possibility of bugs
};

const validateEmailHandler = () => {
	setEmailIsValid(enteredEmail.includes('@'));
    // ❗ it is depending on the other state
    // enteredEmail, not emailIsValid
    // -> has a possibility of bugs
};

const validatePasswordHandler = () => {
	setPasswordIsValid(enteredPassword.trim().length > 6);
    // ❗ it is depending on the other state
    // enteredPassword, not passwordIsValid
    // -> has a possibility of bugs
};

- using userReducer

- using useEffect & useReducer

import { useReducer } from 'react';

// reducerFn should be pronounced outside the component function
// because inside of the reducer function,
// we won't need any data 
// that's generated inside of the component function

// so this reducer function can be created 
// outside of the scope of this component function
// because it dosen't have to interact with anything
// defined inside of the component function

✨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 [formIsValid, setFormIsValid] = useState(false);
    
    // the problem of useEffect is that it runs to often
    // (whenver the emailState or passwordState change)
    // we only want to run setFormIsValid according to validity
    // so we use ✨(object) destructuring
    // ✨ < this is a very general approach >
    
    // ❓ < how about using .someProperty instead of destructuring? >
    // useEffect(() => {
  	// code that only uses someProperty ...
	// }, [someObject.someProperty]);
    // (✖)
    // 왜냐하면 effect 함수는 someObject 가 변경될 때마다 재실행되기 때문이죠 
    // - 단일 속성이 아닙니다
    
    
    const { isValid: emailIsValid } = emailState;
    const { isValid : passwordIsValid } = passwordState;
    
    ✨useEffect(()=> {
      const identifier = setTimeout(() => {
          setFormIsValid(
              ⭐emailIsValid && ⭐passwordIsValid
          )
      }
      , 500);

      return () => { 
          console.log('clean-up');
          clearTimeout(identifier);
      };
	}, [⭐emailIsValid, ⭐passwordIsValid]);
	
    ✨const [emailState, dispatchEmail] = useReducer(emailReducer, {
    	value: '',
        isValid: null,
    });
    
    ✨const [passwordState, dispatchPassword] = useReducer(passwordReducer, {
    	value : '',
        isValie: null,
    });

    const emailChangeHandler = (e) => {
      // setEnteredEmail(e.target.value);
	  ⭐dispatchEmail({type: 'USER_INPUT', val: e.target.value});
      
      // setFormIsValid(
      //     e.target.value.includes('@') && ⭐passwordState.isValid
      // );
      // still not optimal since it depends on two different prev. states
      // -> let's use useEffect()
	};

  const passwordChangeHandler = (e) => {
      // setEnteredPassword(e.target.value);
      ⭐dispatchPassword({type: 'USER_INPUT', val: e.target.value})

     //  setFormIsValid(
     //      ⭐emailState.isValid && e.target.value.trim().length > 6
     //  );
     // still not optimal since it depends on two different prev. states
     // -> let's use useEffect()
  };

  const validateEmailHandler = () => {
      // setEmailIsValid(emailState.isValid);
      ⭐dispatchEmail({type: 'INPUT_BLUR'})
      
  };

  const validatePasswordHandler = () => {
      // setPasswordIsValid(enteredPassword.trim().length > 6);
      ⭐dispatchPassword({type: 'INPUT_BLUR'})
  };
  
  const submitHandler = (e) => {
  	e.preventDefault();
    props.onLogin(⭐emailState.value, ⭐passwordState.value);
  }
}

useState() vs useReducer()

🎈useContext()

: Managing App-Wide or Component-Wide State with Context

  • need unnecessary prop chains

  • so we use "behind-the-scenes" state storage, to connect the components with no direct connection

Method 1 - putting the context logic to App.js

src/store/AuthContext.js

// ✨ Providing Context - the configuration of context (just a dummy)

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

// AuthContext itself is not a component
// it is an object that will contain a component

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

⭐ export default AuthContext;

src/App.js

// since we need the loggedin state in every component,
// we provide it in App.js

const App = () => {
    const [isLoggedIn, setIsLoggedIn] = useState(false);
    
    // a code for handling login/logout state
    useEffect(() => {
      const storedUserLoggedInInformation = localStorage.getItem('isLoggedIn');

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

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

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

	
    return (
    	// ✨ Providing Context
        
    	// the property name should be "value"
        // can make dynamic context by storing a function (ex.logoutHandler)
    	⭐ <AuthContext.Provider 
        value = {{
        	isLoggedIn : isLoggedIn,
            onLogout: logoutHandler
        }} >
        	// components that need a context
        </AuthContext.Provider>
    )
}

Navigation.js

// ✨ listening to it
// ✨ 1. Using Auth-Context consumer

const Navigation = () => {
	return(
    	⭐ <AuthContext.Consumer>
        	⭐ {(ctx) => {
            	return (
                	// JSX code
                    // can approach to context via
                    // ex. ctx.isLoggedIn && ...
                    // onClick = {ctx.onLogout}
                )
            }}
        </AuthContext.Consumer>
    )
}

// ✨ 2. using React-Hook (useContext())

const Navigation = () => {
	
    ⭐ const ctx = useContext(AuthContext);
	
	return(
			// JSX code
            // can approach to context via
            // ex. ctx.isLoggedIn && ...
            // onClick = {ctx.onLogout}
    )
}

Method 2 - seperating the context logic from App.js

src/store/AuthContext.js

// ✨ Providing Context - the configuration of context (just a dummy)

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

// AuthContext itself is not a component
// it is an object that will contain a component

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 loginHander = () => {
    	localStorage.setItem('isLoggedIn', '1');
    	setIsLoggedIn(true);
    };

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

⭐ export default AuthContext;

index.js

root.render(
<AuthContextProvider>
	<App/>
</AuthContextProvider>
);

App.js can focus on rendering components to the screen

function App = () => {
	const ctx = useContext useContext(AuthContext);
}

the component that needs the context just needs useContext to derive it

When to use useContext()?

  • In most cases use props
    because props are mechansim to configure components and to make them reusable

  • Only if there is something which you would forward through a lot of components
    and you're forwarding it to a component that does something very specific
    (ex. Navigation.js - using useContext() O
    common/Button.js - using useContext X)
    you should consider contexts

  • useContext is also inappropriate for high frequency changes
    (ex. state changing for a few seconds frequently)
    => we can solve this problem using Redux

🎈 Hooks의 규칙

profile
🏛️❄💻😻🏠

0개의 댓글

관련 채용 정보