TodoApp은 UI Components를 감싸는 부모 컴포넌트이다. header, AddTodo, TodoList, VisibilityFilters로 구성되어 있다.
import React from "react";
import AddTodo from "./components/AddTodo";
import TodoList from "./components/TodoList";
import VisibilityFilters from "./components/VisibilityFilters";
import "./styles.css";
export default function TodoApp() {
return (
<div className="todo-app">
<h1>Todo List</h1>
<AddTodo />
<TodoList />
<VisibilityFilters />
</div>
);
}
AddTodo는 사용자가 할 일 항목을 입력하고 Add Todo Button을 클릭하여 목록에 추가할 수 있는 컴포넌트이다. Controlled input을 사용해 onChange 이벤트로 상태를 관리하며, 버튼 클릭시 addTodo Action을 dispatch하여 새로운 Todo를 store에 추가한다.
// class component
import React from "react";
import { connect } from "react-redux";
import { addTodo } from "../redux/actions";
class AddTodo extends React.Component {
constructor(props) {
super(props);
this.state = { input: "" };
}
updateInput = input => {
this.setState({ input });
};
handleAddTodo = () => {
this.props.addTodo(this.state.input);
this.setState({ input: "" });
};
render() {
return (
<div>
<input
onChange={e => this.updateInput(e.target.value)}
value={this.state.input}
/>
<button className="add-todo" onClick={this.handleAddTodo}>
Add Todo
</button>
</div>
);
}
}
export default connect(
null,
{ addTodo }
)(AddTodo);
export default AddTodo;
// function component
import React, { useState } from "react";
import { connect } from "react-redux";
import { addTodo } from "../redux/actions";
const AddTodo = ({ addTodo }) => {
const [input, setInput] = useState("");
const updateInput = (input) => {
setInput(input);
};
const handleAddTodo = () => {
addTodo(input);
setInput("");
};
return (
<div>
<input
onChange={(e) => updateInput(e.target.value)}
value={input}
/>
<button className="add-todo" onClick={handleAddTodo}>
Add Todo
</button>
</div>
);
};
export default connect(
null,
{ addTodo }
)(AddTodo);
export default AddTodo;
TodoList는 할 일 리스트 전체를 보여주는 component로써 VisibilityFilters가 선택되었을 때, 필터된 할 일 리스트를 보여준다.
import React from "react";
import { connect } from "react-redux";
import Todo from "./Todo";
// import { getTodos } from "../redux/selectors";
import { getTodosByVisibilityFilter } from "../redux/selectors";
import { VISIBILITY_FILTERS } from "../constants";
const TodoList = ({ todos }) => (
<ul className="todo-list">
{todos && todos.length
? todos.map((todo, index) => {
return <Todo key={`todo-${todo.id}`} todo={todo} />;
})
: "No todos, yay!"}
</ul>
);
// const mapStateToProps = state => {
// const { byIds, allIds } = state.todos || {};
// const todos =
// allIds && state.todos.allIds.length
// ? allIds.map(id => (byIds ? { ...byIds[id], id } : null))
// : null;
// return { todos };
// };
const mapStateToProps = state => {
const { visibilityFilter } = state;
const todos = getTodosByVisibilityFilter(state, visibilityFilter);
return { todos };
// const allTodos = getTodos(state);
// return {
// todos:
// visibilityFilter === VISIBILITY_FILTERS.ALL
// ? allTodos
// : visibilityFilter === VISIBILITY_FILTERS.COMPLETED
// ? allTodos.filter(todo => todo.completed)
// : allTodos.filter(todo => !todo.completed)
// };
};
// export default TodoList;
export default connect(mapStateToProps)(TodoList);
Todo는 TodoList에서의 할 일 중 하나이다. 할 일을 완료되었으면 취소선이 그어지며 onClick시 해당 할 일을 완료 상태를 토글하는 toggleTodo action을 dispatch합니다.
import React from "react";
import { connect } from "react-redux";
import cx from "classnames";
import { toggleTodo } from "../redux/actions";
const Todo = ({ todo, toggleTodo }) => (
<li className="todo-item" onClick={() => toggleTodo(todo.id)}>
{todo && todo.completed ? "👌" : "👋"}{" "}
<span
className={cx(
"todo-item__text",
todo && todo.completed && "todo-item__text--completed"
)}
>
{todo.content}
</span>
</li>
);
// export default Todo;
export default connect(
null,
{ toggleTodo }
)(Todo);
VisibilityFilters는 TodoList를 필터링하는 버튼을 포함하는 컴포넌트이다. all / completed / incomplete 필터 버튼을 클락하여 할 일 목록을 필터링할 수 있다. 선택한 필터를 activeFilter prop를 통해 부모 컴포넌트로부터 받으며 선택된 필터는 밑줄로 강조 표시된다. setFilter action을 dispatch하여 filter를 업데이트한다.
import React from "react";
import cx from "classnames";
import { connect } from "react-redux";
import { setFilter } from "../redux/actions";
import { VISIBILITY_FILTERS } from "../constants";
const VisibilityFilters = ({ activeFilter, setFilter }) => {
return (
<div className="visibility-filters">
{Object.keys(VISIBILITY_FILTERS).map(filterKey => {
const currentFilter = VISIBILITY_FILTERS[filterKey];
return (
<span
key={`visibility-filter-${currentFilter}`}
className={cx(
"filter",
currentFilter === activeFilter && "filter--active"
)}
onClick={() => {
setFilter(currentFilter);
}}
>
{currentFilter}
</span>
);
})}
</div>
);
};
const mapStateToProps = state => {
return { activeFilter: state.visibilityFilter };
};
// export default VisibilityFilters;
export default connect(
mapStateToProps,
{ setFilter }
)(VisibilityFilters);
todos는 할 일 목록을 관리하는 store로써 byIds와 allIds의 state가 존재한다., byIds는 할 일 내용을 담은 content와 할 일을 완료했는 지 확인하는 complete(boolean)으로 구성되어 있는 object이다. allIds는 현재 할 일 리스트에 존재하는 모든 id를 담은 array이다.
const initialState: {
allIds: number[],
byIds: { [key: number]: { content: string, completed: boolean } }
} = {
allIds: [],
byIds: {}
};
visibilityFilters는 현재 어떤 filter가 focus 되었는 지 확인하는 store이다.
export const VISIBILITY_FILTERS = {
ALL: "all",
COMPLETED: "completed",
INCOMPLETE: "incomplete"
};
const initialState = VISIBILITY_FILTERS.ALL;
store는 rootReducer에서 combineReducers를 통해서 선언된 reducer를 통합하고 이후 rootReducer를 createStore에 인자로 넣어 store를 생성한다.
// rootReducer
import { combineReducers } from "redux";
import visibilityFilter from "./visibilityFilter";
import todos from "./todos";
export default combineReducers({ todos, visibilityFilter });
// store
import { createStore } from "redux";
import rootReducer from "./reducers";
export default createStore(rootReducer);
// actionTypes.js
export const ADD_TODO = "ADD_TODO";
export const TOGGLE_TODO = "TOGGLE_TODO";
export const SET_FILTER = "SET_FILTER";
addTodo 새로운 할 일을 TodoList에 추가하는 action creator. 콘텐츠와 함께 id++를 payload로 추가한다.
import { ADD_TODO } from "./actionTypes";
let nextTodoId = 0;
export const addTodo = content => ({
type: ADD_TODO,
payload: {
id: ++nextTodoId,
content
}
});
toggleTodo 특정 할 일의 완료 상태를 토글하는 action creator id를 받아서 해당 할 일을 토글한다.
import { TOGGLE_TODO } from "./actionTypes";
export const toggleTodo = id => ({
type: TOGGLE_TODO,
payload: { id }
});
setFilter 필터를 설정하는 action creator 필터 값(all / completed / incomplete)으로 payload를 받는다.
import { SET_FILTER } from "./actionTypes";
export const setFilter = filter => ({ type: SET_FILTER, payload: { filter } });
ADD_TODO action을 dispatch로 받으면 allIds에 ID를 추가하고 byIds에 해당 ID의 할 일(content)와 완료 여부(completed = default false) 저장한다.
TOGGLE_TODO action을 dispatch로 받으면 해당 ID에 completed의 boolean을 논리 NOT 연산자를 이용하여 반전시킨다.
export default function(state = initialState, action) {
switch (action.type) {
case ADD_TODO: {
const { id, content } = action.payload;
return {
...state,
allIds: [...state.allIds, id],
byIds: {
...state.byIds,
[id]: {
content,
completed: false
}
}
};
}
case TOGGLE_TODO: {
const { id } = action.payload;
return {
...state,
byIds: {
...state.byIds,
[id]: {
...state.byIds[id],
completed: !state.byIds[id].completed
}
}
};
}
default:
return state;
}
}
SET_FILTER action을 dispatch하면 action.payload에 있는 filter를 새로운 값으로 설정한다.
import { SET_FILTER } from "../actionTypes";
const visibilityFilter = (state = initialState, action) => {
switch (action.type) {
case SET_FILTER: {
return action.payload.filter;
}
default: {
return state;
}
}
};
export default visibilityFilter;
`actionType.js``파일에 액션 타입을 상수로 정의해두고 이를 사용하여 action을 dispatch한다.
export const ADD_TODO = "ADD_TODO";
export const TOGGLE_TODO = "TOGGLE_TODO";
export const SET_FILTER = "SET_FILTER";
ActionType.js vs constant.js
공통점으로 둘다 문자열을 변수화 시킨다는 점이다.
하지만 파일명으로 보시다시피 둘 파일은 차이가 분명히 존재한다.
ActionType.js같은 경우Redux Action에서의 type을 하드코딩 하지 않고 문자열 상수화 시켜 추후 중복코드를 방지하고 유지보수성을 높이는데 사용한다.
constant.js같은 경우 값들의 집합이다. 예를 들면VISIBILITY_FILTERS라고 가정해보자.export const VISIBILITY_FILTERS = { ALL: "all", COMPLETED: "completed", INCOMPLETE: "incomplete" };해당 객체는 filter의 값을 모아둔 객체이다. 하드코딩하지 않고 변수화를 시키면 마찬가지로 유지보수성이든, 오타 방지할 수 있다.
결론적으로constant는 단순 상수의 집합 /actionType은 Redux Action 관련된 상수
getTodoList는 todos store에allIds: Array<number>를 받아온다.
export const getTodosState = store => store.todos;
// getTodoState에서 객체가 존재하면 allIds를 반환 아니면 배열
export const getTodoList = store =>
getTodosState(store) ? getTodosState(store).allIds : [];
getTodoById id를 통해 todo를 찾음. 반환값으로 { content: string, complete: boolean, id: number }
// getTodoState(store)로 todo에 대한 store를 불러오고
// byIds[id]로 접근하여 객체 { content: string, complete: boolean }을 가져오고
// 스프레드 연산자를 이용하여 기존 객체에 id를 추가한다.
export const getTodoById = (store, id) =>
getTodosState(store) ? { ...getTodosState(store).byIds[id], id } : {};
getTodos getTodoList를 통해서 allIds를 받아오고, getTodosState를 통해 store에 존재하는 모든 byIds에 있는 객체를 반환한다.
export const getTodos = store =>
getTodoList(store).map(id => getTodoById(store, id));
// 반환값 예시
// [
// { content: "Learn Redux", completed: false, id: 1 },
// { content: "Build a project", completed: true, id: 2 },
// { content: "Deploy the app", completed: false, id: 3 }
// ]
getTodosByVisibilityFilter는 마지막 단계로써 getTodos로 todoList를 불러오고 filter의 값에 따라 todo를 필터해주는 함수이다.
export const getTodosByVisibilityFilter = (store, visibilityFilter) => {
const allTodos = getTodos(store);
switch (visibilityFilter) {
case VISIBILITY_FILTERS.COMPLETED:
return allTodos.filter(todo => todo.completed);
case VISIBILITY_FILTERS.INCOMPLETE:
return allTodos.filter(todo => !todo.completed);
case VISIBILITY_FILTERS.ALL:
default:
return allTodos;
}
};
처음에는 store를 react에 제공하기 위해서 최상단에 <Provide />를 감싸준 후, props에 store를 !
등록해줘야한다.
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import TodoApp from './TodoApp'
import { Provider } from 'react-redux'
import store from './redux/store'
// As of React 18
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<TodoApp />
</Provider>,
)
React Redux는 store에 있는 state를 읽거나 state를 업데이트할 때 다시 읽기 위해 function connect를 제공한다. connect에는 opional인 2개의 argument를 제공한다.
mapStateToProps / mapDispatchToProps
mapStateToProps는 store에 state가 변경될 때마다 호출되며 전체 Redux Store의 state를 받아와서 필요한 데이터만 객체 형태로 반환한다. 즉, 필요한 state를 컴포넌트의 props로 매핑해준다.mapDispathchToProps는 redux의 dispatch를 props로 전달하는 역할을 하며,
function / object형태로 전달한다.function이면 컴포넌트가 생성될 때 한 번만 호출된다. 이 function은 dispatch를 인자로 받고dispatch를 사용하여 액션을 호출하는 함수들이 포함된 객체를 반환해야한다.
onst mapStateToProps = (state, ownProps) => ({
// ... computed data from state and optionally ownProps
})
>
const mapDispatchToProps = {
// ... normally is an object full of action creators
}
>
// `connect` returns a new function that accepts the component to wrap:
const connectToStore = connect(mapStateToProps, mapDispatchToProps)
// and that function returns the connected, wrapper component:
const ConnectedComponent = connectToStore(Component)
>
// We normally do both in one step, like this:
connect(mapStateToProps, mapDispatchToProps)(Component)
// AddTodo Action
import { ADD_TODO } from './actionTypes'
let nextTodoId = 0
export const addTodo = (content) => ({
type: ADD_TODO,
payload: {
id: ++nextTodoId,
content,
},
})
다음은 TodoList에 추가하는 addTodo action creator function이다. 이를 <AddTodo/> Component와 연결하기 위해서는 connect에 mapDispatchToProps addTodo를 넘겨주면 된다.
// components/AddTodo.js
// ... other imports
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'
class AddTodo extends React.Component {
// ... component implementation
}
export default connect(null, { addTodo })(AddTodo)
이렇게 되면 AddTodo Component는 props로 addTodo function을 받게 된다. class component에서의 props를 전달받으려면 this.props.addTodo(state) 이런형식으로 props를 전달받을 수 있다. 또한 addTodo를 dispatch하고, input을 reset할 handleAddTodo가 필요하다.
// 최종 AddTodo Component
// components/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'
class AddTodo extends React.Component {
// ...
/*
handleAddTodo
=> props로 전달받은 addTodo로 입력된 state를 dispatch하고
state input value를 reset하기
*/
handleAddTodo = () => {
// dispatches actions to add todo
this.props.addTodo(this.state.input)
// sets state back to empty string
this.setState({ input: '' })
}
render() {
return (
<div>
<input
onChange={(e) => this.updateInput(e.target.value)}
value={this.state.input}
/>
<button className="add-todo" onClick={this.handleAddTodo}>
Add Todo
</button>
</div>
)
}
}
export default connect(null, { addTodo })(AddTodo)
<TodoList /> Component는 todo가 변경될 때마다 리렌더를 진행해야하기 때문에, connect에 mapStateToProps를 넣어줘야한다.
// components/TodoList.js
// ...other imports
import { connect } from "react-redux";
const TodoList = // ... UI component implementation
const mapStateToProps = state => {
const { byIds, allIds } = state.todos || {};
const todos =
allIds && allIds.length
? allIds.map(id => (byIds ? { ...byIds[id], id } : null))
: null;
return { todos };
};
export default connect(mapStateToProps)(TodoList);
// filter logic
const mapStateToProps = state => {
const { visibilityFilter } = state;
const todos = getTodosByVisibilityFilter(state, visibilityFilter);
return { todos };
};
connect| Do Not Subscribe to the Store | Subscribe to the Store | |
|---|---|---|
| Do Not Inject Action Creators | connect()(Component) | connect(mapStateToProps)(Component) |
| Inject Action Creators | connect(null, mapDispatchToProps)(Component) | connect(mapStateToProps, mapDispatchToProps)(Component) |