import { createStore } from 'redux';
const store = createStore(counterReducer);
const counterSubscriber = () => {
const latestState = store.getState();
}
store.subscribe(counterSubscriber);
// Error - State is undefinced.
const counterReducer = (state, action) => {
return {
counter: state.counter + 1;
}
}
// InitialState 지정 필요
const counterReducer = (state = { counter : 0 }, action) => {
return {
counter: state.counter + 1;
}
}
store.dispatch({ type: 'increment' });
const counterReducer = (state = { counter : 0 }, action) => {
if (action.type === 'increment') {
return {
counter: state.counter + 1;
}
if (action.type === 'decrement') {
return {
counter: state.counter - 1;
}
}
}
/src/store/index.js
import { createStore } from "redux";
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === "increment") {
return { counter: state.counter + 1 };
}
if (action.type === "decrement") {
return { counter: state.counter - 1 };
}
return state;
};
const store = createStore(counterReducer);
export default store;
react-redux
에서 Provider
컴포넌트를 임포트함.import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import store from "./store";
import "./index.css";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />
</Provider>
);
react-redux
의 useSelector 사용.import classes from './Counter.module.css';
import { useSelector } from 'react-redux'
const Counter = () => {
const counter = useSelector((state) => state.counter);
const toggleCounterHandler = () => {};
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
<div className={classes.value}>{counter}</div>
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;
{type: increment}
액션이 디스패치 되도록import classes from "./Counter.module.css";
import { useSelector, useDispatch } from "react-redux";
const Counter = () => {
const dispatch = useDispatch();
const counter = useSelector((state) => state.counter);
const incrementHandler = () => {
dispatch({ type: "increment" });
};
const decrementHandler = () => {
dispatch({ type: "decrement" });
};
const toggleCounterHandler = () => {};
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
<div className={classes.value}>{counter}</div>
<div>
<button onClick={incrementHandler}>Increment</button>
<button onClick={decrementHandler}>Decrement</button>
</div>
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;
connect
Hooks
는 사용 불가함.connect
를 사용 가능! (두 Hook의 기능을 대체)connect(mapStateToProps, mapDispatchToProps)(Component)
import { connect } 'react-redux';
class Counter extends React.Component {
// mapDispatchToProps
incrementHandler() {
this.props.increment();
}
decrementHandler() {
this.props.decrement();
}
toogleCountHandler() {}
render() {
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
<div className={classes.value}>{counter}</div>
<div>
<button onClick={this.incrementHandler}>Increment</button>
<button onClick={this.decrementHandler}>Decrement</button>
</div>
<button onClick={this.toggleCounterHandler}>Toggle Counter</button>
</main>
)
}
}
const mapStateToProps = state => {
return {
counter: state.counter
};
}
const mapDispatchToProps = dispatch => {
return {
increment: () => dispatch({type: 'increment'}),
decrement: () => dispatch({type: 'decrement'});
}
};
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
// store/index.js
import { createStore } from "redux";
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === "increment") {
return { counter: state.counter + 1 };
}
// ✅ Payload 사용
if (action.type === "increase") {
return { counter: state.counter + action.value };
}
if (action.type === "decrement") {
return { counter: state.counter - 1 };
}
return state;
};
const store = createStore(counterReducer);
export default store;
// At Component
const increaseHandler = () => {
dispatch({ type: "increase", amount: 5 });
};
counterReducer
의 initialState 구조를 변경함.import { createStore } from "redux";
const initialState = { counter: 0, showCounter: true };
const counterReducer = (state = initialState, action) => {
if (action.type === "increment") {
return { ...state, counter: state.counter + 1 };
}
if (action.type === "increase") {
return { ...state, counter: state.counter + action.value };
}
if (action.type === "decrement") {
return { ...state, counter: state.counter - 1 };
}
// Toggle 추가
if (action.type === "toggle") {
return {
...state,
showCounter: !state.showCounter,
};
}
return state;
};
const store = createStore(counterReducer);
export default store;
// Counter.jsx
import classes from "./Counter.module.css";
import { useSelector, useDispatch } from "react-redux";
const Counter = () => {
const dispatch = useDispatch();
const counter = useSelector((state) => state.counter);
const show = useSelector((state) => state.showCounter);
const incrementHandler = () => {
dispatch({ type: "increment" });
};
const increaseHandler = () => {
dispatch({ type: "increase", amount: 5 });
};
const decrementHandler = () => {
dispatch({ type: "decrement" });
};
const toggleCounterHandler = () => {};
return (
<main className={classes.counter}>
{show && (
<div className="counter">
<h1>Redux Counter</h1>
<div className={classes.value}>{counter}</div>
<div>
<button onClick={incrementHandler}>Increment</button>
<button onClick={decrementHandler}>Decrement</button>
</div>
</div>
)}
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;
$ npm install @reduxjs/toolkit
${name}Slice
와 같이 사용하면 된다.import { createSlice } from '@reduxjs/toolkit';
createSlice({
name: "counter",
initialState,
reducers: {
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
},
increase(state, action) {
state.counter += action.amount;
},
toggleCounter(state) {
state.showCounter = !state.showCounter;
},
},
});
reducers
가 아닌 reducer
을 넣어줘야 한다.import { createStore } from "redux";
import { createSlice } from "@reduxjs/toolkit";
const initialState = { counter: 0, showCounter: true };
createSlice({
name: "counter",
initialState,
reducers: {
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
},
increase(state, action) {
state.counter += action.amount;
},
toggleCounter(state) {
state.showCounter = !state.showCounter;
},
},
});
const store = createStore(counterSlice.reducer);
export default store;
combineStore
을 해줘야 함.[참고] combineReducers
- redux 라이브러리의 기능으로, 여러 리듀서를 하나의 루트 리듀서로 합쳐줌.
- 참고 문서
createStore
을 이용하는 경우에는 위와 같이 combineReducers를 해주어야 하는 불편함이 있었음.configureStore
을 사용해주면 따로 combineReducers를 사용할 필요 X.// (common) slice가 여러개인 경우, reducer.counter 안에 넣어줘야 함.
const store = configureStore({
reducer: { counter: counterSlice.reducer }
});
const store = configureStore({
reducer: {
todos: todosReducer,
auth: authReducer
}
});
import { createSlice, configureStore } from "@reduxjs/toolkit";
const initialState = { counter: 0, showCounter: true };
createSlice({
name: "counter",
initialState,
reducers: {
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
},
increase(state, action) {
state.counter += action.payload;
},
toggleCounter(state) {
state.showCounter = !state.showCounter;
},
},
});
const store = configureStore({
reducer: counterSlice.reducer,
});
export default store;
${name}Slice.actions.${reducerName}
을 호출하면 된다.counterSlice.actions.increase();
// return { type: 'some identifier' }
export const counterActions = counterSlice.actions;
import { counterActions } from "../store/index";
...
const incrementHandler = () => {
dispatch(counterActions.increment());
};
const decrementHandler = () => {
dispatch(counterActions.decrement());
};
const increaseHandler = () => {
dispatch(counterActions.increase(5)); // action.payload
};
[주의] action.payload
- 이전에는 아래와 같이 payload 필드의 이름을 지정할 수 있었지만,
- redux-toolkit에서는 액션을 자동으로 생성하고, 추가 데이터는 payload라는 이름으로 받아오기 때문에 임의로 필드명을 수정할 수 없다.
// 1. 기존 방식
const increaseHandler = () => {
dispatch({ type: "increase", amount: 5 });
};
// 2. Redux-Toolkit (Slice)
const incrementHandler = () => {
dispatch(counterActions.increase(5));
};
// 1. 기존 방식
if (action.type === "increase") {
return { counter: state.counter + action.amount };
}
// 2. Reduxt-Toolkit
increase(state, action) {
state.counter += action.payload;
},
// App.js
import Counter from "./components/Counter";
import Header from "./components/Header";
import Auth from "./components/Auth";
import { Fragment } from "react";
function App() {
return (
<Fragment>
<Header />
<Auth />
<Counter />
</Fragment>
);
}
export default App;
import { createSlice, configureStore } from "@reduxjs/toolkit";
const initialState = { counter: 0, showCounter: true };
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
},
increase(state, action) {
state.counter += action.payload;
},
toggleCounter(state) {
state.showCounter = !state.showCounter;
},
},
});
const store = configureStore({
reducer: { counter: counterSlice.reducer },
});
export const counterActions = counterSlice.actions;
export default store;
import { createSlice, configureStore } from "@reduxjs/toolkit";
const initialCounterState = { counter: 0, showCounter: true };
const initialAuthState = {
isAuthenticated: false,
};
const counterSlice = createSlice({
name: "counter",
initialState: initialCounterState,
reducers: {
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
},
increase(state, action) {
state.counter += action.payload;
},
toggleCounter(state) {
state.showCounter = !state.showCounter;
},
},
});
const authSlice = createSlice({
name: "auth",
initialState: initialAuthState,
reducers: {
login(state) {
state.isAuthenticated = true;
},
logout(state) {
state.isAuthenticated = false;
},
},
});
const store = configureStore({
reducer: { counter: counterSlice.reducer, auth: authSlice.reducer },
});
export const counterActions = counterSlice.actions;
export const authActions = authSlice.actions;
export default store;
// 기존 코드
const counter = useSelector(state => state.counter);
// 변경 코드
const counter = useSelector({counter} => counter.counter);
// Auth.jsx
import classes from "./Auth.module.css";
import { useDispatch } from "react-redux";
import { authActions } from "../store/index";
const Auth = () => {
const dispatch = useDispatch();
const handleLogin = () => {
dispatch(authActions.login());
};
return (
<main className={classes.auth}>
<section>
<form>
<div className={classes.control}>
<label htmlFor="email">Email</label>
<input type="email" id="email" />
</div>
<div className={classes.control}>
<label htmlFor="password">Password</label>
<input type="password" id="password" />
</div>
<button onClick={handleLogin}>Login</button>
</form>
</section>
</main>
);
};
export default Auth;
// Header.jsx
import classes from "./Header.module.css";
import { useDispatch } from "react-redux";
import { authActions } from "../store/index";
const Header = () => {
const dispatch = useDispatch();
const handleLogout = () => {
dispatch(authActions.logout());
}
return (
<header className={classes.header}>
<h1>Redux Auth</h1>
<nav>
<ul>
<li>
<a href="/">My Products</a>
</li>
<li>
<a href="/">My Sales</a>
</li>
<li>
<button onClick={handleLogout}>Logout</button>
</li>
</ul>
</nav>
</header>
);
};
export default Header;
// App.js
import { useSelector } from "react-redux";
import Counter from "./components/Counter";
import Header from "./components/Header";
import Auth from "./components/Auth";
import UserProfile from "./components/UserProfile";
import { Fragment } from "react";
function App() {
const isLogined = useSelector((state) => state.auth.isAuthenticated);
return (
<Fragment>
<Header />
{isLogined ? <UserProfile /> : <Auth />}
<Counter />
</Fragment>
);
}
export default App;
// Header.jsx
import classes from "./Header.module.css";
import { useDispatch, useSelector } from "react-redux";
import { authActions } from "../store/index";
const Header = () => {
const dispatch = useDispatch();
const isLogined = useSelector((state) => state.auth.isAuthenticated);
const handleLogout = () => {
dispatch(authActions.logout());
};
return (
<header className={classes.header}>
<h1>Redux Auth</h1>
{isLogined && (
<nav>
<ul>
<li>
<a href="/">My Products</a>
</li>
<li>
<a href="/">My Sales</a>
</li>
<li>
<button onClick={handleLogout}>Logout</button>
</li>
</ul>
</nav>
)}
</header>
);
};
export default Header;
/store/index.js 안에 다 작성되어있는 코드를 분할해보자.
// store/index.js
import { configureStore } from "@reduxjs/toolkit";
import counterSlice from "./counter";
import authSlice from "./auth";
const store = configureStore({
reducer: { counter: counterSlice.reducer, auth: authSlice.reducer },
});
export default store;
// store/counter.js
import { createSlice } from "@reduxjs/toolkit";
const initialCounterState = { counter: 0, showCounter: true };
const counterSlice = createSlice({
name: "counter",
initialState: initialCounterState,
reducers: {
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
},
increase(state, action) {
state.counter += action.payload;
},
toggleCounter(state) {
state.showCounter = !state.showCounter;
},
},
});
export const counterActions = counterSlice.actions;
export default counterSlice;
// store/auth.js
import { createSlice } from "@reduxjs/toolkit";
const initialAuthState = {
isAuthenticated: false,
};
const authSlice = createSlice({
name: "auth",
initialState: initialAuthState,
reducers: {
login(state) {
state.isAuthenticated = true;
},
logout(state) {
state.isAuthenticated = false;
},
},
});
export const authActions = authSlice.actions;
export default authSlice;