$ npx create-react-app react-redux --template typescript
$ npm install redux --save
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
Clicked: times
<button>+</button>
<button>-</button>
</div>
);
}
export default App;
src/reducers/index.tsx 생성
const counter = (state = 0, action: { type: string }) => {
switch (action.type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
default:
break;
}
}
export default counter;
store.getState()
- 애플리케이션의 현재 상태 트리를 반환합니다. 스토어의 리듀서가 마지막으로 반환한 값과 동일합니다.
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import counter from './reducers';
import { createStore } from 'redux';
const store = createStore(counter);
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App
value={store.getState()}
onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
/>
</React.StrictMode>
);
App 컴포넌트의 props로 해당 값들을 보내줌.
// App.tsx
import React from 'react';
import logo from './logo.svg';
import './App.css';
type Props = {
value: number;
onIncrement: () => void;
onDecrement: () => void;
};
function App({ value, onIncrement, onDecrement }: Props) {
return (
<div className="App">
Clicked: {value} times
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
);
}
export default App;
// App.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import counter from './reducers';
import { createStore } from 'redux';
const store = createStore(counter);
const render = () => {
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App
value={store.getState()}
onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
/>
</React.StrictMode>
);
};
render();
store.subscribe(render);
// reducers/todos.tsx
enum ActionType {
ADD_TODO = 'ADD_TODO',
DELETE_TODO = 'DELETE_TODO',
}
interface Action {
type: ActionType;
text: string;
}
const todos = (state = [], action: Action) => {
// Action.text 값을 받아옴 (Payload)
switch (action.type) {
case 'ADD_TODO':
return [...state, action.text];
default:
return state;
}
};
export default todos;
[참고] TypeScript의 enum
- Union(|)을 이용해도 좋음.
- enum을 사용해 리터럴의 타입과 값에 이름을 붙여서 코드의 가독성을 크게 높일 수 있음.
- enum은 객체이다. (단, 객체와 달리 enum의 속성은 변경할 수 없음.)
// reducers/index.tsx
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';
const rootReducer = combineReducers({
counter,
todos,
});
export default rootReducer;
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
const render = () => {
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App
value={store.getState()}
onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
/>
</React.StrictMode>
);
};
render();
store.subscribe(render);
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import rootReducer from './reducers';
import { Provider } from 'react-redux';
const store = createStore(rootReducer);
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<Provider store={store}>
<App
onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
/>
</Provider>
</React.StrictMode>
);
참고 - event 객체
e: React.ChangeEvent<HTMLInputElement>
e: FormEvent<HTMLFormElement>
// App.tsx
import React, { ChangeEvent, FormEvent, useState } from 'react';
import logo from './logo.svg';
import './App.css';
type Props = {
onIncrement: () => void;
onDecrement: () => void;
};
function App({ onIncrement, onDecrement }: Props) {
const [todoValue, setTodoValue] = useState('');
const addTodo = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setTodoValue('');
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setTodoValue(e.target.value);
};
return (
<div className="App">
Clicked: times
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
<form onSubmit={addTodo}>
<input type="text" value={todoValue} onChange={handleChange} />
<input type="submit" />
</form>
</div>
);
}
export default App;
// App.tsx
const counter = useSelector((state) => state.counter);
// Error: 'state'은(는) 'unknown' 형식입니다.ts(18046)
state:RootState
와 같이 타입 지정을 해줘야 함.[참고] ReturnType
ReturnType<T>
- 함수 T의 반환 타입으로 정의한다. Parameters와 대칭되는 형태임.
- 참고 링크: https://typescript-kr.github.io/pages/utility-types.html#returntypet
// reducer/index.tsx
export type RootState = ReturnType<typeof rootReducer>;
// rootReducer가 반환하는 타입. (ReturnType)
// App.tsx
import { RootState } from './reducers';
const todos = useSelector((state: RootState) => state.todos);
const counter = useSelector((state: RootState) => state.counter);
const loggerMiddleware = (store) => (next) => (action) => {
console.log(store, action);
next(action)
}
const middleware = applyMiddleware(loggerMiddleware);
const store = createStore(rootReducer, middleware); // 수정
enum ActionType {
FETCH_POSTS = 'FETCH_POSTS',
DELETE_POSTS = 'DELETE_POSTS',
}
interface Post {
userId: number;
id: number;
title: string;
}
interface Action {
type: ActionType;
payload: Post[];
}
const posts = (state = [], action: Action) => {
switch (action.type) {
case 'FETCH_POSTS':
return [...state, ...action.payload];
default:
return state;
}
};
export default posts;
// App.tsx
useEffect(() => {
dispatch(fetchPosts());
}, [dispatch]);
const fetchPosts = (): any => {
return async function fetchPostsThunk(dispatch: any, getState: any) {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/posts'
);
dispatch({ type: 'FETCH_POSTS', text: todoValue });
};
};
// index.tsx
import thunk from 'redux-thunk';
const middleware = applayMiddleware(thunk, loggerMiddleware); // thunk를 추가
// App.tsx
const posts: Post[] = useSelector((state: RootState) => state.posts);
...
{posts.map((post, index) => <li key={index}>{post.title}</li>}
// actions/posts.tsx
import axios from 'axios';
const fetchPosts = (): any => {
return async function fetchPostsThunk(dispatch: any, getState: any) {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/posts'
);
dispatch({ type: 'FETCH_POSTS', text: response.data });
};
};
export default fetchPosts;
import axios from 'axios';
export const fetchPosts = () => async (dispatch: any, getState: any) => {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/posts'
);
dispatch({ type: 'FETCH_POSTS', text: response.data });
};
$ npx create-react-app my-app --template redux-typescript
Redux Toolkit
1. React에 Redux 스토어 제공
- Provider 컴포넌트
2. Redux State Slice 생성
- createSlice를 통해 슬라이스 생성
- Initial State가 무엇인지, Slice 이름이 무엇인지, 어떻게 변경되는지를 선언.
- Reducer 함수에서 변경 로직을 작성 가능함.
-> immer 라이브러리를 쓰기 때문에 이전처럼 불변성때문에 머리아플 일 없다!3. Store에 Slice Reducer 생성
- 슬라이스에서 리듀서 함수를 가져와서 스토어에 추가 (
configureStore
)- 리듀서 Params 내부에 필드 정의하여 스토어에 리듀서 함수를 사용하여 state를 업데이트하도록.
4. Redux State 및 Actions 사용
- useSelector, useDispatch 사용
const INCREMENT = 'counter/increment'; // type
function increment(amount: number) { // 생성자 함수
return {
type: INCREMENT,
payload: amount
}
}
const action = increment(10); // action.payload로 10을 넘겨줌
import { createAction } from '@reduxjs/toolkit';
const increment = createAction<number>('counter/increment'); // type, 생성자 함수
const action = increment(10);
const counterReducer = createReducer(initialState, (builder) => {
builder
.addCase(increment, (state, action) => {
state.value++
})
.addCase(decrement, (state, action) => {
state.value--
})
.addCase(incrementByAmount, (state, action) => {
state.value += action.payload
})
})
[참고] builder은 무엇인가?
- createReducer에서 액션 객체를 처리하기 위해 case reducer을 정의하는 방법이 두가지가 있다.
빌더 콜백 (Builder Callback)
- 맵 객체 (Map Object) 표기법.
- 동일하지만, 빌더 콜백이 TypeScript와의 호환성 때문에 더 선호된다.
builder.addCase(type, 생성자)
- 액션 타입과 정확히 맵핑되는 케이스 리듀서 추가- 그 외에도 addMatcher (주어진 패턴과 일치하는지 체크 후 처리), addDefaultCase 등이 있다.
import { createAction, nanoid } from '@reduxjs/toolkit'
const addTodo = createAction('todos/add', function prepate(text) {
return {
payload: {
text,
id: nanoid(),
createdAt: new Date().toISOString()
}
}
})
console.log(addTodo('abc'));
// { type: 'todos/add', payload: {text: 'abc', id: '342413fgkskso', createdAt: '2019-10-03~..'}
import { createSlice } from '@reduxjs/toolkit';
const initialState = { value: 0 };
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: { // case reducer 함수들의 객체
increment(state) { // type: counter/increment
state.value++
},
decrement(state) { // type: counter/decrement
state.value--
},
incrementByAmount(state, action) { // type: counter/incrementByAmount
state.value += action.payload
}
}
})
function createAsyncThunk(type, payloadCreator, options)
useEffect return 부분에 (컴포넌트 언마운트 시) promise.abort()
를 실행하여
비동기 요청 도중 취소되도록 구현 가능
thunkName/rejected가 디스패치됨 (createAsyncThunk 참고)
abort 되었을 때, request도 취소되도록 하려면?
-> new AbortController()