Provider
컴포넌트가 항상 최상위로 동작해야 합니다.1) Single source of truth
2) State is read-only
3) Changes are made with pure functions
![[Pasted image 20240124130312.png]]
1) Store (스토어)
2) Action (액션)
{
type: 'ACTION_CHANGE_USER', // 필수
payload: // 옵션
{
name: '하피몬',
age: 130
}
}
3) Reducer (리듀서)
4) Dispatch (디스패치)
Redux가 이런 공식을 따르는 이유는 데이터가 한 방향으로만 흘러야 하기 때문이다.
![[Pasted image 20240124131046.png]]
1) mapStateToProps
2) Redux hooks - 최근에 나온 기술
npm install redux react-redux
// reducers/cartReducer.js
/** 1. Redux 사용을 위해 빈배열을 초기값으로 변수를 생성합니다. */
let initialValue = [];
function cartReducer(state = initialValue, action) {
let copyValue = [...state]; // 2. state는 불변성을 가져야 하므로 state를 복사해줍니다.
/** 장바구니에 아이템을 추가하는 예시 입니다. */
// *3. action.type 은 `ADD_CART` 입니다.*
/** 4. findIndex를 이용하여 state에 담긴 데이터 중 id가 일치하는 데이터를 찾습니다. state에 담긴 값은 Store에 저장되어 있는 값입니다.*/
if (action.type === 'ADD_CART') {
const findIndexDataByIdData = copyValue.findIndex(
item => item.id === id,
);
/** 5. findIndexDataByIdData가 -1이면 state에 담긴 데이터 중 id가 일치하는 데이터가 없기 때문에 받아온 데이터인 action.payload 데이터를 state에 push합니다. */
/** 6. findIndexDataByIdData가 -1이 아니면 state에 담긴 데이터 중 id가 일치하는 데이터가 있기 때문에 해당 데이터의 count를 증가시킵니다. */
// *action.payload에는 dispatch로 받아온 업데이트 될 데이터가 저장되어 있습니다.*
if (findIndexDataByIdData === -1) {
copyValue.push(action.payload);
} else {
copyValue[findIndexDataByIdData].count += action.payload.count;
}
return copyValue; // 7. 값을 업데이트 한 copyValue를 return 해줍니다.
/** 7. 추가적인 예시를 위해 전체 삭제 기능의 예시를 추가하였습니다. **/
} else if (action.type === 'DELETE_ALL_CART') {
return (copyValue = []); // 해당 action이 호출되면 copyValue의 값을 빈배열로 반환합니다.
/** 8. redux의 마지막은 state를 return 해주어야 에러가 발생하지 않습니다. */
} else {
return state;
}
}
만약 하나의 reducer 만 사용한다면 rootReducer를 정의할 필요가 없습니다. 바로 store에 넣으면 됨
Reducer를 한 개만 넣을 경우
function reducer(state = initialValue, action) { ... 내용 생략 } export default reducer;
넣어야 할 Reducer가 여러 개 일 경우
import { combineReducers } from "redux"; function cartReducer(state = initialValue, action) { ... 내용 생략 } function deliveryReducer(state = initialValue, action) { ... 내용 생략 } const rootReducer = combineReducers({ cartReducer, deliveryReducer, }) export default rootReducer;
전역 상태이므로 최상위 Provider에 주입합니다.
const store = createStore(reducer);
return (
<Provider store={store}>
<Router />
</Provider>
);
const store = createStore(rootReducer);
return (
<Provider store={store}>
<Router />
</Provider>
);
import { useSelector } from 'react-redux';
/** order 페이지에 데이터를 넘겨주기 위해 useSelector를 이용하여 state 값을 cartData 변수에 저장합니다. */
const cartData = useSelector(state => {
return state.cart; // state에서 cart라는 name을 가지고 있는 데이터를 반환합니다.
});
import { useState } from 'react';
import { useDispatch } from 'react-redux';
const [detailData, setDetailData] = useState({});
const dispatch = useDispatch(); // redux의 dispatch를 편하게 사용하기 위해 변수화 해줍니다.
/** 이벤트가 발생할 때마다 상태값을 변경해주기 위해 함수를 만들어 줍니다. **/
const putInCartData = () => {
dispatch({ // 1. dispatch 함수를 이용해 상태를 변화시킬 action.type을 설정해줍니다.
type: 'ADD_CART',
payload: { // 2. payload로 변화된 상태값을 객체형태로 데이터를 넣어줍니다.
id: detailData?.id,
},
});
}
Reducer에서 반환하는 상태 변경 전에 어떠한 동작을 처리하고 싶을 때 사용하는 MiddleWare
- 순서
middleware 미사용 시 : action → reducer → store
middleware 사용 시 : action → middleware → reducer → store
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import logger from 'redux-logger';
import ReduxThunk from 'redux-thunk';
const store = createStore(reducer, applyMiddleware(ReduxThunk, logger));
redux-logger는 dispatch될 때 action의 타입과 변경 전 상태와 변경 후 상태를 콘솔에 출력해주는 패키지 입니다.
npm i redux-logger
redux-logger 의 경우에는 middleware의 인자로 넣어주면 되지만 순서는 항상 맨 마지막에 위치해야 한다!
const store = createStore(reducer, applyMiddleware(ReduxThunk, logger));
redux-thunk는 가장 많이 사용되는 미들웨어로써 비동기 작접을 처리할 때 용이합니다. 이 패키지를 사용하면 dispatch 할 때, 액션 타입을 객체가 아닌 함수를 dispatch 할 수 있습니다.
npm i redux-thunk
const getComments = () => (dispatch, getState) => {
// 이 안에서는 액션을 dispatch 할 수도 있고
// getState를 사용하여 현재 상태도 조회 할 수 있습니다.
const id = getState().post.activeId;
// 요청이 시작했음을 알리는 액션
dispatch({ type: 'GET_COMMENTS' });
// 댓글을 조회하는 프로미스를 반환하는 getComments 가 있다고 가정해봅시다.
api
.getComments(id) // 요청을 하고
.then(comments => dispatch({ type: 'GET_COMMENTS_SUCCESS', id, comments })) // 성공시
.catch(e => dispatch({ type: 'GET_COMMENTS_ERROR', error: e })); // 실패시
};
// 액션 타입
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
// getState를 쓰지 않는다면 굳이 파라미터로 받아올 필요 없습니다.
export const increaseAsync = () => dispatch => {
setTimeout(() => dispatch(increase()), 1000);
};
export const decreaseAsync = () => dispatch => {
setTimeout(() => dispatch(decrease()), 1000);
};
// 초깃값 (상태가 객체가 아니라 그냥 숫자여도 상관 없습니다.)
const initialState = 0;
export default function counter(state = initialState, action) {
switch (action.type) {
case INCREASE:
return state + 1;
case DECREASE:
return state - 1;
default:
return state;
}
}