Today I Learned ... React.js
🙋♂️ Reference Lecture
Redux Lecture
- React Redux
- Redux Toolkit
🔻 기존 Hooks를 이용한 Todo App은?
import { useState } from 'react';
const Home = () => {
const [text, setText] = useState('');
const [todos, setTodos] = useState([]);
const onChangeInput = (e) => {
setText(e.target.value);
};
const onSubmitForm = (e) => {
e.preventDefault();
setTodos([...todos, text]);
console.log(todos);
setText('');
};
return (
<>
<h1>To Do</h1>
<form onSubmit={onSubmitForm}>
<input type="text" value={text} onChange={onChangeInput} />
<button>Add</button>
</form>
<ul>
{todos.map((todo) => (
<li>{todo}</li>
))}
</ul>
</>
);
};
src/store.js
import { createStore } from 'redux';
const ADD = 'ADD';
const DELETE = 'DELETE';
export const addToDo = (text) => {
return {
type: ADD,
text,
};
};
export const deleteToDo = (id) => {
return {
type: DELETE,
id,
};
};
const reducer = (state = [], action) => {
switch (action.type) {
case ADD:
return [{ text: action.text, id: Date.now() }, ...state];
case DELETE:
return state.filter((toDo) => toDo.id !== action.id);
default:
return state;
}
};
const store = createStore(reducer);
export default store;
react-redux의 Provider 컴포넌트를 임포트한 후,
store을 props로 전달해줌.
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import store from './store';
import { Provider } from 'react-redux';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './routes/Home';
import Detail from './routes/Detail';
const App = () => {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/:id" element={<Detail />} />
</Routes>
</Router>
);
};
export default App;
이제는 useSelector과 useDispatch 훅을 사용하는 것을 권장한다.
function mapStateToProps(state, ownProps)
-> redux store로부터 state를 받아옴.
반드시 무언가를 return 해야함.
import { useState } from 'react';
import { connect } from 'react-redux';
const Home = (props) => {
console.log(props);
const [text, setText] = useState('');
const [todos, setTodos] = useState([]);
const onChangeInput = (e) => {
setText(e.target.value);
};
const onSubmitForm = (e) => {
e.preventDefault();
setTodos([...todos, text]);
setText('');
};
return (
<>
<h1>To Do</h1>
<form onSubmit={onSubmitForm}>
<input type="text" value={text} onChange={onChangeInput} />
<button>Add</button>
</form>
<ul>
{todos.map((todo) => (
<li>{todo}</li>
))}
</ul>
</>
);
};
// 추가한 부분
const getCurrentState = (state, ownProps) => {
return { cute: true };
};
export default connect(getCurrentState)(Home);
🔻 수정
const mapStateToProps = (state) => {
return { toDos: state };
// toDos 를 현재 컴포넌트(Home)의 props로 지정함
};
export default connect(mapStateToProps)(Home);
-> store과 컴포넌트를 연결한 것. (props로 지정해주면서)
export default connect(mapStateToProps, mapDispatchToProps)(Home);
...
const mapDispatchToProps = (dispatch) => {
console.log(dispatch);
};
export default connect(mapStateToProps, mapDispatchToProps)(Home);
dispatch 함수 구조는 다음과 같다.
function dispatch(action) {
if (!isPlainObject(action)) {
throw new Error( // 생략 )
}
if (typeof action.type === 'undefined') {
throw new Error( // 생략 );
}
if (isDispatching) {
throw new Error( // 생략);
}
try {
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
var listeners = currentListeners = nextListeners;
for (var i = 0; i < listeners.length; i++) {
var listener = listeners[i];
listener();
}
return action;
// 액션을 return하는 함수 = dispatch
}
import { useState } from 'react';
import { connect } from 'react-redux';
import { actionCreators } from '../store';
const Home = ({ toDos, addTodo }) => {
const [text, setText] = useState('');
const onChangeInput = (e) => {
setText(e.target.value);
};
const onSubmitForm = (e) => {
e.preventDefault();
// 🔻 dispatch 함수 호출. (Payload인 text 전달)
addTodo(text);
setText('');
};
return (
<>
<h1>To Do</h1>
<form onSubmit={onSubmitForm}>
<input type="text" value={text} onChange={onChangeInput} />
<button>Add</button>
</form>
<ul>{JSON.stringify(toDos)}</ul>
// 🔺 배열을 문자열 그대로 보여줌
</>
);
};
const mapStateToProps = (state) => {
return { toDos: state };
};
const mapDispatchToProps = (dispatch) => {
// 🔻 addToDo (액션생성함수)를 디스패치함.
return {
addTodo: (text) => dispatch(actionCreators.addToDo(text)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Home);
-> id는 Date.now()
로 저장됨.
case ADD:
return [{ text: action.text, id: Date.now() }, ...state];
const Todo = ({ text, id }) => {
return (
<li>
{text} <button>DEL</button>
</li>
);
};
export default Todo;
import { useState } from 'react';
import { connect } from 'react-redux';
import { actionCreators } from '../store';
import ToDo from '../components/ToDo';
const Home = ({ toDos, addTodo }) => {
const [text, setText] = useState('');
const onChangeInput = (e) => {
setText(e.target.value);
};
const onSubmitForm = (e) => {
e.preventDefault();
addTodo(text);
setText('');
};
return (
<>
<h1>To Do</h1>
<form onSubmit={onSubmitForm}>
<input type="text" value={text} onChange={onChangeInput} />
<button>Add</button>
</form>
<ul>
// 🔻 하위 컴포넌트로 props 전달 (text, id)
{toDos.map((todo) => (
<ToDo text={todo.text} id={todo.id} key={todo.id} />
))}
</ul>
</>
);
};
const mapStateToProps = (state) => {
return { toDos: state };
};
const mapDispatchToProps = (dispatch) => {
return {
addTodo: (text) => dispatch(actionCreators.addToDo(text)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Home);
✅ 참고 - spread 이용 Props
{toDos.map((toDo) => ( <ToDo text={toDo.text} id={toDo.id} key={toDo.id} /> ))}
🔻
{toDos.map((toDo) => ( <ToDo {...toDo} key={toDo.id} /> ))}
import { connect } from 'react-redux';
import { actionCreators } from '../store';
const Todo = ({ text, deleteToDo }) => {
const onClickButton = () => {
deleteToDo();
};
return (
<li>
{text} <button onClick={onClickButton}>DEL</button>
</li>
);
};
const mapDispatchToProps = (dispatch, ownProps) => {
return {
deleteToDo: () => dispatch(actionCreators.deleteToDo(ownProps.id)),
};
};
export default connect(null, mapDispatchToProps)(Todo);
delete 할때는 store의 state를 가져올 필요는 없고, dispatch만 하면 된다.
-> connect의 첫번째 인자로 null을 넣어준다.
mapDispatchToProps
의 두번째 매개변수인 ownProps를 보면 ownProps.id가 id에 해당한다.
-> payload인 id로 넣어주면 된다.
import { connect } from 'react-redux';
import { actionCreators } from '../store';
const Todo = ({ text, id, deleteToDo }) => {
const onClickButton = () => {
deleteToDo(id);
};
return (
<li>
{text} <button onClick={onClickButton}>DEL</button>
</li>
);
};
const mapDispatchToProps = (dispatch) => {
return {
deleteToDo: (id) => dispatch(actionCreators.deleteToDo(id)),
};
};
export default connect(null, mapDispatchToProps)(Todo);
ownProps가 아닌 직접 Todo에서 props를 구조분해로 받은 다음, deleteToDo의 인자로 전달해주는 방법도 된다.
❗️ 참고 - 아래와 같이 deleteToDo를 직접 넣어줘도 OK.
return ( <li> {text} <button onClick={() => deleteToDo(id)}>DEL</button> </li> );
❔ 언제 ownProps를 쓰고 언제 직접 값을 대입하나?
- 해당 컴포넌트의 props를 인자로 넣어줘야 할 때는
ownProps
를 쓰자. (ToDo)
- 해당 컴포넌트의 props가 아닌 ref나 state를 넣어줘야 할 때는 직접 인자로 받아 대입하자. (Home)
ToDo.jsx 수정
import { connect } from 'react-redux';
import { actionCreators } from '../store';
import { Link } from 'react-router-dom';
const Todo = ({ text, deleteToDo, id }) => {
return (
<li>
<Link to={`/${id}`}>
{text} <button onClick={deleteToDo}>DEL</button>
</Link>
</li>
);
};
const mapDispatchToProps = (dispatch, ownProps) => {
return {
deleteToDo: () => dispatch(actionCreators.deleteToDo(ownProps.id)),
};
};
export default connect(null, mapDispatchToProps)(Todo);
/:id
로 이동하는 Link 컴포넌트 추가함
Detail.jsx
import { useParams } from 'react-router-dom';
const Detail = () => {
const { id } = useParams();
return <div>Detail {id}</div>;
};
export default Detail;
-> useParams() 사용함.
Detail.jsx 수정
import { useParams } from 'react-router-dom';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
const Detail = ({ toDo }) => {
const { id } = useParams();
const toDoText = toDo.find((toDo) => toDo.id === parseInt(id));
return (
<>
<h1>{toDoText.text}</h1>
<h5>Created at : {id}</h5>
<Link to="/">Home</Link>
</>
);
};
const mapStateToProps = (state) => {
return {
toDo: state,
};
};
export default connect(mapStateToProps)(Detail);