리덕스 개요
리덕스 Action
리덕스 Reducers
createStore, combineReducers
Context 로 연결하기
react-redux
[Homework] 개발 서적 평가 서비스에 리덕스 적용하기
Software Engineer | Studio XID, Inc.
Microsoft MVP
TypeScript Korea User Group Organizer
Electron Korea User Group Organizer
Marktube (Youtube)
단일 스토어다!
[만들기] 단일 스토어 사용 준비하기
import redux
액션을 정의하고,
액션을 사용하는, 리듀서를 만들고,
리듀서들을 합친다.
최종 합쳐진 리듀서를 인자로, 단일 스토어를 만든다.
[사용하기] 준비한 스토어를 리액트 컴포넌트에서 사용하기
import react-redux
connect 함수를 이용해서 컴포넌트에 연결
npx create-react-app redux-start
cd redux-start
npm i redux
function 액션생성자(...args) { return 액션; }
액션의 타입을 정의하여 변수로 빼는 단계
강제는 아닙니다. (그러므로 안해도 됩니다.)
그냥 타입을 문자열로 넣기에는 실수를 유발할 가능성이 큽니다.
미리 정의한 변수를 사용하면, 스펠링에 주의를 덜 기울여도 됩니다.
액션 객체를 만들어 내는 함수를 만드는 단계
하나의 액션 객체를 만들기 위해 하나의 함수를 만들어냅니다.
액션의 타입은 미리 정의한 타입 변수로 부터 가져와서 사용합니다.
// actions.js
// 액션의 type 정의
// 액션의 타입 => 액션 생성자 이름
// ADD_TODO => addTodo
export const ADD_TODO = 'ADD_TODO';
// 액션 생산자
// 액션의 타입은 미리 정의한 타입으로 부터 가져와서 사용하며,
// 사용자가 인자로 주지 않습니다.
export function addTodo(text) {
return { type: ADD_TODO, text }; // { type: ADD_TODO, text: text }
}
function 리듀서(previousState, action) {
return newState;
}
// reducers.js
import { ADD_TODO } from './actions';
export function todoApp(previousState, action) {
if (previousState === undefined) {
return [];
}
if (action.type === ADD_TODO) {
return [...previousState, { text: action.text }];
}
return previousState;
}
redux 로 부터 import
const store = createStore(리듀서);
// store.js
import { todoApp } from './reducers';
import { createStore } from 'redux';
import { addTodo } from './actions';
const store = createStore(todoApp);
console.log(store);
console.log(store.getState());
setTimeout(() => {
store.dispatch(addTodo('hello'));
}, 1000);
export default store;
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import store from './store';
store.subscribe(() => {
const state = store.getState();
console.log('store changed', state);
});
ReactDOM.render(<App />, document.getElementById('root'));
serviceWorker.unregister();
action 을 정의하고, action 생성자를 만들고, reducer 를 수정
// actions.js
// 액션의 type 정의
// 액션의 타입 => 액션 생성자 이름
// ADD_TODO => addTodo
export const ADD_TODO = 'ADD_TODO';
export const COMPLETE_TODO = 'COMPLETE_TODO';
// 액션 생산자
// 액션의 타입은 미리 정의한 타입으로 부터 가져와서 사용하며,
// 사용자가 인자로 주지 않습니다.
export function addTodo(text) {
return { type: ADD_TODO, text }; // { type: ADD_TODO, text: text }
}
// actions.js
// 액션의 type 정의
// 액션의 타입 => 액션 생성자 이름
// ADD_TODO => addTodo
export const ADD_TODO = 'ADD_TODO';
export const COMPLETE_TODO = 'COMPLETE_TODO';
// 액션 생산자
// 액션의 타입은 미리 정의한 타입으로 부터 가져와서 사용하며,
// 사용자가 인자로 주지 않습니다.
export function addTodo(text) {
return { type: ADD_TODO, text }; // { type: ADD_TODO, text: text }
}
export function completeTodo(index) {
return { type: COMPLETE_TODO, index }; // { type: COMPLETE_TODO, index: index}
}
import { ADD_TODO, COMPLETE_TODO } from './actions';
export function todoApp(previousState, action) {
if (previousState === undefined) {
return [];
}
if (action.type === ADD_TODO) {
return [...previousState, { text: action.text, completed: false }];
}
if (action.type === COMPLETE_TODO) {
const newState = [];
for (let i = 0; i < previousState.length; i++) {
newState.push(
i === action.index
? { ...previousState[i], completed: true }
: { ...previousState[i] },
);
}
return newState;
}
return previousState;
}
// store.js
import { todoApp } from './reducers';
import { createStore } from 'redux';
import { addTodo, completeTodo } from './actions';
const store = createStore(todoApp);
console.log(store);
console.log(store.getState());
setTimeout(() => {
store.dispatch(addTodo('hello'));
setTimeout(() => {
store.dispatch(completeTodo(0));
}, 1000);
}, 1000);
export default store;
[
{
text: 'Hello',
completed: false
}
]
{
todos: [
{
text: 'Hello',
completed: false
}
],
filter: 'SHOW_ALL'
}
import { ADD_TODO, COMPLETE_TODO } from './actions';
export function todoApp(previousState, action) {
if (previousState === undefined) {
return { todos: [], filter: 'SHOW_ALL' };
}
if (action.type === ADD_TODO) {
return {
todos: [...previousState.todos, { text: action.text, completed: false }],
filter: previousState.filter,
};
}
if (action.type === COMPLETE_TODO) {
const todos = [];
for (let i = 0; i < previousState.todos.length; i++) {
todos.push(
i === action.index
? { ...previousState.todos[i], completed: true }
: { ...previousState.todos[i] },
);
}
return { todos, filter: previousState.filter };
}
return previousState;
}
export function todos(previousState, action) {
if (previousState === undefined) {
return [];
}
if (action.type === ADD_TODO) {
return [...previousState.todos, { text: action.text, completed: false }];
}
if (action.type === COMPLETE_TODO) {
const newState = [];
for (let i = 0; i < previousState.length; i++) {
newState.push(
i === action.index
? { ...previousState[i], completed: true }
: { ...previousState[i] },
);
}
return newState;
}
return previousState;
}
export function filter(previousState, action) {
if (previousState === undefined) {
return 'SHOW_ALL';
}
return previousState;
}
export function todoApp(previousState = {}, action) {
return {
todos: todos(previousState.todos, action),
filter: filter(previousState.filter, action),
};
}
import { combineReducers } from 'redux';
const todoApp = combineReducers({
todos,
filter,
});
// App.js
import React, { useState, useEffect } from 'react';
import './App.css';
import { addTodo } from './actions';
function App({ store }) {
const [state, setState] = useState(store.getState());
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setState(store.getState());
});
return () => {
unsubscribe();
};
});
return (
<div className="App">
<header className="App-header">
<p>{JSON.stringify(state)}</p>
<button
onClick={() => {
store.dispatch(addTodo('Hello'));
}}
>
추가
</button>
</header>
</div>
);
}
export default App;
import React from 'react';
const ReduxContext = React.createContext();
export default ReduxContext;
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App2';
import * as serviceWorker from './serviceWorker';
import store from './store';
import ReduxContext from './context';
ReactDOM.render(
<ReduxContext.Provider value={store}>
<App />
</ReduxContext.Provider>,
document.getElementById('root'),
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
import React, { useContext } from 'react';
import './App.css';
import { addTodo } from './actions';
import ReduxContext from './context';
import Button from './Button';
class App extends React.Component {
static contextType = ReduxContext;
_unsubscribe;
state = this.context.getState();
componentDidMount() {
this._unsubscribe = this.context.subscribe(() => {
this.setState(this.context.getState());
});
}
componentWillUnmount() {
this._unsubscribe();
}
render() {
return (
<div className="App">
<header className="App-header">
<p>{JSON.stringify(this.state)}</p>
<Button />
</header>
</div>
);
}
}
export default App;
import React, { useContext } from 'react';
import { addTodo } from './actions';
import ReduxContext from './context';
export default function Button() {
const store = useContext(ReduxContext);
return (
<button
onClick={() => {
store.dispatch(addTodo('Hello'));
}}
>
추가
</button>
);
}
npm i react-redux
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App3';
import * as serviceWorker from './serviceWorker';
import store from './store';
import { Provider } from 'react-redux';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
import React, { useContext, useEffect, useState } from 'react';
import { ReactReduxContext } from 'react-redux';
import './App.css';
import { addTodo } from './actions';
import Button from './Button';
class App extends React.Component {
render() {
console.log(this.props);
return (
<div className="App">
<header className="App-header">
<p>{JSON.stringify(this.props.todos)}</p>
<Button add={this.props.add} />
</header>
</div>
);
}
}
function AppContainer(props) {
const { store } = useContext(ReactReduxContext);
const [state, setState] = useState(store.getState());
function add(text, dispatch) {
console.log(text, dispatch);
dispatch(addTodo(text));
}
useEffect(() => {
const _unsubscribe = store.subscribe(() => {
setState(store.getState());
});
return () => {
_unsubscribe();
};
});
return (
<App
{...props}
todos={state.todos}
add={text => add(text, store.dispatch)}
/>
);
}
export default AppContainer;
import React from 'react';
export default function Button({ add }) {
return <button onClick={() => add('hello')}>추가</button>;
}
import React from 'react';
import './App.css';
import { addTodo } from './actions';
import { connect } from 'react-redux';
import Button from './Button';
class App extends React.Component {
render() {
return (
<div className="App">
<header className="App-header">
<p>{JSON.stringify(this.props.todos)}</p>
<Button add={this.props.add} />
</header>
</div>
);
}
}
const mapStateToProps = state => {
return { todos: state.todos };
};
const mapDispatchToProps = dispatch => {
return {
add: text => {
dispatch(addTodo(text));
},
};
};
const AppContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(App);
export default AppContainer;
import React from 'react';
export default function Button({ add }) {
return <button onClick={() => add('hello')}>추가</button>;
}
const mapStateToProps = state => {
return { todos: state.todos };
};
const mapDispatchToProps = dispatch => {
return {
add: text => {
dispatch(addTodo(text));
},
};
};