Typescript에서 useState hook을 사용할 때 해당 상태가 어떤 타입을 가지는지 Generics를 사용하여 설정해준다.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState<number>(0); // Generics 사용
const onIncrease = () => setCount(count + 1);
const onDecrease = () => setCount(count - 1);
return (
<div>
<h1>{count}</h1>
<div>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
</div>
);
}
export default Counter;
하지만, Generics를 생략해도 상관없다. 알아서 타입을 잘 유추하기 때문이다
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const onIncrease = () => setCount(count + 1);
const onDecrease = () => setCount(count - 1);
return (
<div>
<h1>{count}</h1>
<div>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
</div>
);
}
export default Counter;
하지만 꼭 써야할 경우가 존재한다. 바로 상태가 null일 수도 있고 아닐 수도 있을 때 사용한다.
type Information = { name: string; description: string };
const [info, setInformation] = useState<Information | null>(null);
추가적으로 상태의 타입이 까다로운 구조를 가진 객체이거나 배열일 때는 Generics를 명시하는 것이 좋다.
type Todo = { id: number; text: string; done: boolean };
const [todos, setTodos] = useState<Todo[]>([]);
배열인 경우에는 빈 배열만 넣었을 때 해당 값이 어떤 Type인지 유추할 수 없기 때문에 Generics를 명시해야 한다
import React, {useState} from 'react';
function Counter(){
const [count, setCount] = useState(0);
const onIncrease = () => setCount(count + 1);
const onDecrease = () => setCount(count - 1);
return(
<div>
<h1>{count}</h1>
<div>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
</div>
);
}
import React, {useReducer} from 'react';
type Action = {type : 'INCREASE'} | {type : 'DECREASE'};
function reducer(state : number, action : Action) : number {
// 리듀서를 만들 땐 이렇게 파라미터로 받아오는 상태의 타입과 함수가 리턴하는 타입을 동일하게 하는 것이 매우 중요합니다.
switch(action.type){
case 'INCREASE' :
return state + 1;
case 'DECREASE' :
return state - 1;
default :
throw new Error('Unhandled action');
}
}
function Counter(){
const [count, dispatch] = useReducer(reducer, 0);
const onIncrease = () => dispatch({type : 'INCREASE'});
const onDecrease = () => dispatch({type : 'DECREASE'});
return(
<div>
<h1>{count}</h1>
<div>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
</div>
);
}
export default Counter;
Typescript 환경에서 Redux 사용하기
yarn add redux react-redux @types/react-redux
redux의 경우에는 자체적으로 Typescript 지원이 되지만 react-redux의 경우 그렇지 않으므로 @types를 붙여서 패키지를 설치해야한다.
src/modules/counter.ts
1) 액션 type 선언
const INCREASE = 'counter/INCREASE' as const;
const DECREASE = 'counter/DECREASE' as const;
const INCREASE_BY = 'counter/INCREASE_BY' as const;
as const는 const assertions 문법 → 추후 액션 생성함수를 통해 액션 객체를 만들게 됐을 때 type의 Typescript타입이 string이 되지 않고 실제 값을 가리키게 됨
2) 액션 생성 함수 선언
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseBy = (diff: number) => ({
type: INCREASE_BY,
payload: diff
})
추후 액션 생성 함수들은 컨테이너 컴포넌트에서 불러와서 사용을 해야하므로
3) 액션 객체들에 대한 type 설정
type CounterAction =
| ReturnType<typeof increase>
| ReturnType<typeof decrease>
| ReturnType<typeof increaseBy>;
type : Typescript의 타입
나중에 리듀서를 작성 할 때 action 파라미터의 타입을 설정하기 위해서 모든 액션들의 TypeScript 타입을 준비해주어야 한다.
ReturnType : 함수에서 반환하는 타입을 가져올 수 있게 해주는 유틸 타입
이전의 액션에 type 값들을 선언 할 때 as const를 사용하지 않으면 ReturnType을 사용하게 됐을때 type 의 타입이 무조건 string으로 처리됨
4) 상태의 타입과 초기값 선언
type CounterState = {
count: number;
}
const initialState: CounterState = {
count: 0
};
5) 리듀서 작성
function counter(state: CounterState = initialState, action: CounterAction) {
switch (action.type) {
case INCREASE:
return { count: state.count + 1 };
case DECREASE:
return { count: state.count - 1 };
case INCREASE_BY:
return { count: state.count + action.payload };
default:
return state;
}
}
export default counter;
함수의 반환 타입에 상태의 타입을 넣는 것 잊지말기!
1) 루트 리듀서 생성
import { combineReducers } from 'redux';
import counter from './counter';
const rootReducer = combineReducers({
counter
});
export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;
Javascript와 다른 점은 RootState라는 타입을 만들어서 내보내 줘야 함
→ 추후 컨테이너 컴포넌트를 만들 때 스토어에서 관리하고 있는 상태를 조회하기 위해서 useSelector를 사용할 때 필요함
index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './modules';
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
프리젠테이셔널 컴포넌트와 컨테이너 컴포넌트는 분리해도 되고 안 해도 상관 없다.
src/components/Counter.tsx
프리젠테이셔널 컴포넌트 생성
// 프리젠테이셔널 컴포넌트 / 컨테이너 컴포넌트 분리
import React from 'react';
type CounterProps = {
count: number;
onIncrease: () => void;
onDecrease: () => void;
onIncreaseBy: (diff: number) => void;
};
function Counter({
count,
onIncrease,
onDecrease,
onIncreaseBy
}: CounterProps) {
return (
<div>
<h1>{count}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
<button onClick={() => onIncreaseBy(5)}>+5</button>
</div>
);
}
export default Counter;
src/containers/CounterContainter.tsx
컨테이너 컴포넌트 생성
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../modules';
import { increase, decrease, increaseBy } from '../modules/counter';
import Counter from '../components/Counter';
function CounterContainer() {
const count = useSelector((state: RootState) => state.counter.count);
const dispatch = useDispatch();
const onIncrease = () => {
dispatch(increase());
};
const onDecrease = () => {
dispatch(decrease());
};
const onIncreaseBy = (diff: number) => {
dispatch(increaseBy(diff));
};
return (
<Counter
count={count}
onIncrease={onIncrease}
onDecrease={onDecrease}
onIncreaseBy={onIncreaseBy}
/>
);
}
export default CounterContainer;
src/App.tsx
import React from 'react';
import CounterContainer from './containers/CounterContainer';
function App() {
return <CounterContainer />;
}
export default App;
src/hooks/useCounter.tsx
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../modules';
import { increase, decrease, increaseBy } from '../modules/counter';
import { useCallback } from 'react';
export default function useCounter() {
const count = useSelector((state: RootState) => state.counter.count);
const dispatch = useDispatch();
const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
const onIncreaseBy = useCallback(
(diff: number) => dispatch(increaseBy(diff)),
[dispatch]
);
return {
count,
onIncrease,
onDecrease,
onIncreaseBy
};
}
src/components/Counter.tsx
import React from 'react';
import useCounter from '../hooks/useCounter';
function Counter() {
const { count, onIncrease, onDecrease, onIncreaseBy } = useCounter();
return (
<div>
<h1>{count}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
<button onClick={() => onIncreaseBy(5)}>+5</button>
</div>
);
}
export default Counter;
필요한 함수와 값을 props로 받아오는 것이 아닌, useCounter hook을 통해서 받아옴
src/App.tsx
import React from 'react';
import Counter from './components/Counter';
function App() {
return <Counter />;
}
export default App;