: Working with (Side) Effects
=> so we should not directly code side effects to the component
=> 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
an input example (whether the form is valid or invalid)
useEffect(()=> {
setFormIsValid(
enteredEmail.includes('@') && enteredPassword.trim().length > 6
)
}, [enteredEmail, enteredPassword])
(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 및 구성 요소와 독립적이며 변경되지 않습니다.
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])
: 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
};
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);
}
}
: Managing App-Wide or Component-Wide State with Context
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}
)
}
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
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