$ npm i redux
$ npm i react-redux
- redux | 모든 js 프로젝트에서 사용할 수 있는 state management 패키지
- react-redux | 리액트 어플리케이션에서 redux의 store/reducer에 간단히 접속할 수 있도록 도와주는 패키지
- src/store 폴더를 만들어서 redux와 관련된 파일을 관리한다.
- redux dev tool | redux toolkit을 이용하면 확장 프로그램을 설치만 하면 사용할 수 있다. 아니면 추가적으로 코드를 작성해야 한다.
redux with react
컴포넌트에 store 제공하기 | provider
// src/store/index.js
import { legacy_createStore } from 'redux'
import { INCREMENT } from '../store/index'
// const redux = require('redux');
// export const INCREMENT = 'increment';
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === INCREMENT) {
return { ...state, counter: state.counter + action.amount };
}
if (action.type === 'decrement') {
return { ...state, counter: state.counter - 1 };
}
return state;
};
const store = legacy_createStore(counterReducer);
export default store;
// index.js
import { Provider } from 'react-redux';
import store from './store/index';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
- store를 만들고, 제공한다.
- 리액트에 의해 가장 먼저 실행되는 index.js 파일에 store를 제공한다.
- <Provider> component | provider의 하위 컴포넌트에서 redux에 접근할 수 있다.
- 해당 store를 subscribe하고, reducer에 action을 dispatch하는 것은 컴포넌트에서 작업한다.
- INCREMENT 상수를 만들어서 export하면, 외부 파일에서 action을 dispatch할 때 오타가 생기는 것을 막을 수 있다.
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === 'increment') {
state.counter++; // DON'T DO THIS!!!
return state;
}
/*...*/
}
- 절대 기존의 state를 변형(mutate)하지 않는다. 예측할 수 없는, 디버깅 하기 어려운 버그를 만들 수 있기 때문이다.
- 항상 새로운 객체/배열을 반환해야 한다. immutable data with primitive, reference type
useSelector | component에서 store에 접근하기
import {useSelector} from 'react-redux';
const Counter = () => {
const counter = useSelector(state => state.counter);
return (
<main>
<h1>Redux Counter</h1>
<p>COUNTER VALUE:{counter}</p>
<button>Toggle Counter</button>
</main>
);
};
export default Counter;
- useSelector( (state) => { } )
- 리액트 리덕스에서 제공하는 커스텀 훅이다. => 리액트 리덕스에 의해 실행된다.
- 리액트 리덕스는 이 훅을 호출하고 있는 컴포넌트를 자동으로 subscriber로 등록한다. 즉, state가 변경되면 subscriber(컴포넌트)를 재호출한다.
- 콜백 함수에서 store에서 관리하고 있는 state 중 일부(part)를 선택할 수 있다.
- 해당 컴포넌트가 unmount 상태가 되면, subscription이 clear 된다.
- +) class component에서는 connect api를 이용한다.
- +) 직접 store 전체에 접근하는 useStore 커스텀 훅도 있다.
useDispatch | component에서 action을 dispatch 하기
import { useSelector, useDispatch } from 'react-redux';
const Counter = () => {
const counter = useSelector(state => state.counter);
const dispatch = useDispatch();
const incrementHandler = () => {
dispatch({type:'increment', amount:5})
}
const decrementHandler = () => {
dispatch({type:'decrement'})
}
return (
<main>
<h1>Redux Counter</h1>
<p>COUNTER VALUE:{counter}</p>
<button onClick={incrementHandler}>increment</button>
<button onClick={decrementHandler}>decrement</button>
</main>
);
};
export default Counter;
- useDispatch
- 아무런 인자도 받지 않고, dispatch를 실행할 수 있는 함수를 반환한다.
- dispatch의 인자로 action을 전달한다.
redux toolkit
$ npm i @reduxjs/toolkit
react-redux에서 만든 또 다른 패키지로, 리덕스를 이용해서 상태를 관리하는 것을 도와주는 툴이다. toolkit에는 redux 패키지가 포함되어 있다.
import { createSlice, configureStore } from '@reduxjs/toolkit'
const initalState = { counter: 0, isVisible: true };
const counterSlice = createSlice({
name:'counter',
initialState: initalState,
reducers : {
increment: (state, action)=>{
// return {...state, counter:state.counter + action.payload}
state.counter = state.counter + action.payload;
}
decrement: (state, action)=>{
state.counter--;
}
}
})
const store = configureStore({
// reducer: {counter : counterSlice.reducer, auth:...}
reducer: counterSlice.reducer;
});
export default store;
export const counterActions = counterSlice.actions;
- configureStore( {reducer: /* ... */} ) | 인자로 reducer라는 프로퍼티를 갖는 객체를 전달해야 한다.
- redux 패키지의 create store처럼, store를 만드는 api이다.
- 이 때 configureStore는 createStore와 달리 여러 개의 reducer를 하나의 reducer로 합칠 수 있다(merge).
- 최종적으로 redux는 항상 global state를 관리하는 단 하나의 reducer 만을 갖는다.
- slice가 여러 조각일 때, store에 연결해야 할 reducer도 많아지기 때문에 유용하게 사용할 수 있다.
- +) redux에도 위와 같은 역할을 하는 combineReducers라는 api가 있다.
- createSlice | 특정 상태를 관리하는 조각을 만들 수 있다.
- 예를 들어 counter, authorization과 관련된 state를 각각 분리하는 조각을 만들 수 있다.
- name, initialState, reducers라는 프로퍼티를 갖는다.
- create slice는 모든 reducer에 자동으로 고유한 action을 할당한다. => 개발자는 따로 action을 지정할 필요가 없다.
- state immutability
- 리덕스 toolkit은 내부적으로 immer라는 패키지를 사용하고 있다.
- 이 패키지는 reducer 함수 내에서 state를 직접 변형하는 코드(ex. state.count++)를 변환해서, 실제로는 state immutability를 유지할 수 있게 해준다.
import { useDispatch, useSelector, } from 'react-redux';
import { counterActions } from '../store/index';
const Counter = () => {
// 여러 개의 slice를 사용하고 있을 때
// const isVisible = useSelector(state => state.counter.isVisible);
const dispatch = useDispatch();
const incrementHandler = () => {
dispatch(counterActions.increment(5));
}
}
- conterActions = counterSlice.actions;
- reducer 메서드의 이름을 이용하면, ex) counterSlice.actions.increment(), 자동으로 action 객체가 생성되고 그 값이 dispatch의 인자로 할당된다.
- action 객체는 다음과 같이 구성되어있고, increment(5)의 5는 payload 프로퍼티의 값으로 전달된다.
- { type: unique identifier, payload: additional data...}
- increment: (state, action) => { state.count = state.count + action.payload; }
async code with redux
- reducer function | 일반적인 개념으로서 reducer는 다음과 같은 조건을 만족해야 한다.
- 순수함수여야 한다. | why reducer should be a pure function?
- 리덕스는 old state를 각 reducer 함수에 input으로 전달하고, 각 reducer 함수가 새로운 state를 반환할 것을 기대한다.
- 이 때 새로운 state와 old state를 비교해서, 변화가 없다면 기존의 state를, 어떤 변화가 있다면 새로운 state를 반환한다.
- 이 때 새로운 state와 old state 객체가 저장되어있는 메모리 주소를 비교하기 때문에, reducer 함수가 input을 mutate하지 않는 순수함수 여야 하는 것이다. => 실제 어플리케이션에서 deep comparison은 highly cost task이기 때문에 사용되지 않는다.
- ??? 코드 내부에 side effect가 없어야 한다.
- side effect 코드는 unpredictable 하기 때문에...? 순수함수와 달리 무언가를 바꾸기 때문에???
- ex) http request, read local-storage...
- 순수함수여야 한다. | why reducer should be a pure function?
- side effect 코드는 reducer 함수 밖에서 작성되어야 한다.
- useEffect | 컴포넌트에서 useEffect 실행
- action creator fn | redux toolkit이 자동으로 만드는 action 대신, action을 커스텀해서 사용한다.
useEffect
let isInitial = true;
function App() {
const orderList = useSelector((state) => state.cart.orderList);
const notification = useSelector((state) => state.ui.notification);
const dispatch = useDispatch();
useEffect(() => {
const sendData = async() => {
// try ...
const res = await fetch(
'https://react-http-1de5e-default-rtdb.asia-southeast1.firebasedatabase.app/orders.json',
{
method: 'PUT',
body: JSON.stringify({ orderList }),
}
);
if (!res.ok) throw new Error('sending cart data failed');
dispatch(
uiActions.showNotification({
status: 'success',
title: 'Success',
message: 'sent your cart data!',
})
);
// catch ...
}
if(isInitial) {
isInitial = false;
return ;
}
sendData();
}, [orderList, dispatch]);
return (
/*...*/
);
}
- useSelector | 컴포넌트가 state를 subscribe한다. => orderList가 변경될 때 마다 app 컴포넌트가 재평가된다.
- useEffect | orderList state가 업데이트 될 때 마다 콜백함수를 실행한다.
- useEffect는 dependency가 변할 때 뿐만 아니라, app이 시작 될 때에도 실행된다. => isInitial
- 즉, orderList가 텅 비어있는 초기 상태에서도 백엔드에 요청을 전송한다.
- isInitial
- 컴포넌트 밖에서 isInitial을 정의 => 앱이 렌더링 될 때 마다 해당 값이 재평가되지 않도록 한다.
- app이 처음 렌더링 될 때 sendData를 실행하지 않도록 한다.
- PUT | 백엔드가 처리하는 로직으로, 기존의 특정 데이터를 수정한다(override).
- 컴포넌트가 무거워진다.
action creator & thunk
- thunk | a function that delays an action untill later
- 어떤 일이 끝날 때 까지 action을 반환하는 것을 미루는 함수.
- action creator | action을 만든다.
- ex) uiSlice.actions.showNotification({ status:...}) => redux tool kit이 자동으로 action을 만들어 반환해 준다.
- action creator를 thunk로 작성한다.
- action을 반환하는 것을 미룰 수 있다.
- 대신 action creator는 action을 직접 반환하는 것이 아니라, action을 반환하는 함수를 반환하게 된다.
- 즉 action을 dispatch 하기 전에, 다른 코드를 실행할 수 있다.
- 이 때 action creator는 slice 외부에 작성한다.
- 컴포넌트를 가볍게(lean) 유지할 수 있다.
// app.js
useEffect(() => {
if (isInit) {
isInit = false;
return;
}
dispatch(sendCartData(orderList));
}, [orderList, dispatch]);
// store/cart.js
const sendCartData = (cartData) => {
return async(dispatch)=>{
// do some async tasks
dispatch(/*some actions with cartData*/)
}
}
- dispatch의 인자로 전달한 sendCart는 action creator 함수이다.
- action creator는 action을 반환하는 함수를 반환하는데, 이 함수에서 dispatch를 이용해 action을 반환한다.
// store/cart.js
const cartInitState = {}
const cartSlice = {
name:'cart'
initialState: cartInitState,
reducers: {
addItem:()=>{},
removeItem:()=>{},
}
}
export default cartSlice;
export const cartActions = cartSlice.actions;
export const sendCartData = (cartData) => {
return async (dispatch) => {
try {
dispatch(
uiActions.showNotification({
status: 'pending',
title: 'Pending',
message: 'sending your cart data...',
})
);
const res = await fetch(
'https://react-http-1de5e-default-rtdb.asia-southeast1.firebasedatabase.app/orders.json',
{
method: 'PUT',
body: JSON.stringify({ cartData }),
}
);
if (!res.ok) throw new Error('sending cart data failed');
dispatch(
uiActions.showNotification({
status: 'success',
title: 'Success',
message: 'sent your cart data!',
})
);
} catch (error) {
dispatch(
uiActions.showNotification({
status: 'error',
title: 'Error',
message: `failed to send cart data`,
})
);
}
};
};
'Study > npm Libraries' 카테고리의 다른 글
React with web accessibility (0) | 2022.09.19 |
---|---|
TanStack Query(React Query) (0) | 2022.07.24 |
React-router(v6) (0) | 2022.07.03 |
React-router (0) | 2022.07.01 |
what is Redux? (0) | 2022.06.29 |