React Redux Hooks with Typescript
React & redux & Hook with typescript
Create sample application, and initial setup.
Create demo application with CRA
$ yarn create react-app react-redux-hooks-tutorial –typescript
Install redux, react-redux
$ yarn add redux react-redux@next redux-devtools-extension
$ yarn add @types/react-redux @types/redux-devtools-extension
Create below directories under src
- components/
- containers/
- modules/
Create Counter redux module
src/modules/counter.ts
1. Declare action types.
When you declare type, you should have to attach as const
keyword.
const INCREASE = 'counter/INCREASE' as const;
const DECREASE = 'counter/DECREASE' as const;
const INCREASE_BY = 'counter/INCREASE_BY' as const;
as const
is a TypeScript grammer, called as const assertions.
When we create action object using action creator, type’s type isn’t a string
, actual value itself.
2. Create action creation function
when create action creators, use can use function
keyword or arrow function.
With arrow, we could omit return phrase.
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseBy = (diff: number) => ({
type: INCREASE_BY,
payload: diff
});
increase
decrease
wouldn’t receive any parameter from function.
In case of increaseBy
, get diff
value as parameter, and set action’s payload
.
name of payload
follows FSA rule
all the action creators with export
, so we can use it containers.
3. Preparation type for the action objects
We should have to prepare actions, which we’ve made, for set action parameter’s type while writing reducer.
type CounterAction =
| ReturnType<typeof increase>
| ReturnType<typeof decrease>
| ReturnType<typeof increaseBy>;
ReturnType
is a util type, that helps us getting type which is returned from function.
The usage of as const
keyword, makes ReturnType
make the type with specific values, not a common string
itself.
4. Declare state’s type, initial state
Declare state’s type and state’s intial value.
type CounterState = {
count: number;
}
const initialState: CounterState = {
count: 0
};
You can use type
or interface
when declaring state’s type.
5. Create reducer
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;
}
}
Apply Redux to the project
Now, We’ll apply redux to our project. We have only one reducer, counter, but we’ll create more reducer in the future. So, now we’ll create root reducer.
modules/index.ts
import { combineReducers } from 'redux';
import counter from './counter';
const rootReducer = combineReducers({
counter
});
export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;
Similar with javascript code, but you should have to create RootState
type, and export it.
This type is needed when we use useSelector
for accessing state which are stored in the store, while creating container component.
Now, create store, and apply redux into react project with Provider component.
index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
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')
);
// 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();
Create Counter presentational component
Follows Presentational and Container Components but it’s not mandatory.
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;
All the values and function which are needed in components are handled with props.
Create Counter Container component
containers/CounterContainer.tsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Counter from '../components/Counter';
import { RootState } from '../modules/index';
import { increase, decrease, increaseBy } from '../modules/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;
Check in useSelector
phrase, state
type as RootState
Now, render this CounterContainer at App component.
App.tsx
import React from 'react';
import CounterContainer from './containers/CounterContainer';
const App: React.FC = () => {
return <CounterContainer />;
}
export default App;
If we do not seperate Presentational/ Container ?
“Hooks let me do the same thing without an arbitrary division,” original
When you use a component, do not use props, create a custom Hook with useSelector
and useDispatch
, use it.
We’ll create useCounter
custom Hook.
hooks/useCounter.tsx
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../modules/index';
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
};
}
Like composing Container, but not a component, as Hook.
Now, you can this useCounter
hook at Presentational Component.
Ah, from now on, there’s no need to distingish presentational and container.
Fix Counter.tsx and index.tsx
components/Counter.tsx
import React from 'react';
import useCounter from '../hooks/useCounter';
type CounterProps = {
count: number;
onIncrease: () => void;
onDecrease: () => void;
onIncreaseBy: (diff: number) => void;
}
const Counter: React.FC = () => {
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;
All that needed are from useCounter
Hook, not from props.
Now, we don’t need containers, so remove containers
directory.
All will render Counter.
App.tsx
import React from 'react';
import Counter from './components/Counter';
const App: React.FC = () => {
return <Counter />;
}
export default App;
Before Hooks, when we composing Container component, comminucate between component and redux through connect()
as HOC pattern
Creating ToDo List redux module
Now, more complecated redux module, ToDo List. In this time, payload, which required while crateing action object, value will be different for each action.
create todos.ts file in modules directory.
modules/todos.ts
action type / action creation function/ action type declare
// Action type
const ADD_TODO = 'todos/ADD_TODO' as const;
const TOGGLE_TODO = 'todos/TOGGLE_TODO' as const;
const REMOVE_TODO = 'todos/REMOVE_TODO' as const;
// Action creator
export const addTodo = (text: string) => ({
type: ADD_TODO,
payload: text
});
export const toggleTodo = (id: number) => ({
type: TOGGLE_TODO,
payload: id
});
export const removeTodo = (id: number) => ({
type: REMOVE_TODO,
payload: id
});
// Action type
type TodosAction =
| ReturnType<typeof addTodo>
| ReturnType<typeof toggleTodo>
| ReturnType<typeof removeTodo>;
declare state type, and initial state
// Declare state type
export type Todo = {
id: number;
text: string;
done: boolean;
};
type TodoState = Todo[];
const initialState: TodosState = [
{ id: 1, text: 'Learn typescript', done: true },
{ id: 2, text: 'Use typescript with redux', done: true },
{ id: 3, text: 'Making Todo List', done: false }
];
type Todo
was exported because it would be used at component later.
Now, create reducer
function todos(state: TodosState = initialState, action: TodosAction): TodosState {
switch(action.type) {
case ADD_TODO:
const nextId = Math.max(...state.map(todo => todo.id)) +1;
return state.concat({
id: nextId,
text: action.payload,
done: false,
});
case TOGGLE_TODO:
return state.map(todo =>
todo.id === action.payload ? {...todo, done: !todo.done} : todo
);
case REMOVE_TODO:
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
}
export default todos;
register to root reducer
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';
const rootReducer = combineReducers({
counter,
todos
});
export default rootReducer; // ducks pattern
export type RootState = ReturnType<typeof rootReducer>;
Prepare ToDo List component
We’ll create 3 components
- TodoInsert: Add new item
- TodoItem: Show Todo item
- TodoList: Show multiple TodoItem component
components/TodoInsert.tsx
TodoInsert component is a component for add a new item.
Status of Input will be managed by local state with useState.
We can use useDispatch
within this component, but we’ll use custom Hook.
import React, { ChangeEvent, FormEvent, useState }from 'react';
const TodoInsert: React.FC = () => {
const [value, setValue] = useState('');
const onChange= (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const onSubmit = (e:FormEvent) => {
e.preventDefault();
// TODO: Add new item using custom Hook
setValue('');
};
return (
<form onSubmit={onSubmit}>
<input
placeholder="Input todo item."
value={value}
onChange={onChange}
/>
<button type="submit">Add</button>
</form>
);
}
export default TodoInsert;
// TODO will be implemented later with custom Hook.
components/TodoItem.tsx
TodoItem component will show the information of what to do.
When click text, done
value will be changed.
When click (x) right side, it will be deleted.
Get todo
from props. and toggling and removal function onToggle
, onRemove
will be implemented later
(ofc, onToggle
and onRemove
could be put into props)
import React from 'react';
import './TodoItem.css';
import { Todo } from '../modules/todos';
type TodoItemProps = {
todo: Todo;
};
function TodoItem({ todo }: TodoItemProps) {
// TODO: Implement onToggle/ onRemove with Custom Hook
return (
<li className={`TodoItem ${todo.done ? 'done': ''}`}>
<span className='text'>{todo.text}</span>
<span className='remove'>(X)</span>
</li>
);
}
export default TodoItem;
components/TodoItem.css
Create a css for TodoItem component
.TodoItem .text {
cursor: pointer;
}
.TodoItem.done .text {
color: #999999;
text-decoration: line-through;
}
.TodoItem .remove {
color: red;
margin-left: 4px;
cursor: pointer;
}
components/TodoList.tsx
TodoList Component.
It will look up todos
array which stored by redux store with custom Hook.
import React from 'react';
import { Todo } from '../modules/todos';
import TodoItem from './TodoItem';
const TodoList: React.FC = () => {
const todos: Todo[] = []; // Look up with custom Hook
if (!todos.length) {
return <p>No items</p>;
}
return (
<ul>
{todos.map(todo => (
<TodoItem todo={todo} key={todo.id} />
))}
</ul>
);
}
export default TodoList;
Render above Components from App
App.tsx
import React from 'react';
import Counter from './components/Counter';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
const App: React.FC = () => {
return (
<>
<TodoInsert />
<TodoList />
</>
);
}
export default App;
Implement custom Hook
useTodos hook
useTodos Hook will look up todo items.
hooks/useTodos.ts
import { useSelector } from 'react-redux';
import { RootState } from '../modules/index';
import { Todo } from '../modules/todos';
const useTodos: () => Todo[] = () => {
const todos = useSelector((state: RootState) => state.todos);
return todos;
}
export default useTodos;
components/TodoList.tsx
import React from 'react';
import { Todo } from '../modules/todos';
import TodoItem from './TodoItem';
import useTodos from '../hooks/useTodos';
const TodoList: React.FC = () => {
const todos: Todo[] = useTodos();
if (!todos.length) {
return <p>No items</p>;
}
return (
<ul>
{todos.map(todo => (
<TodoItem todo={todo} key={todo.id} />
))}
</ul>
);
}
export default TodoList;
useAddTodo Hook
useAddTodo
will register todo item
hooks/useAddTodo.ts
import { useDispatch } from 'react-redux';
import { useCallback } from 'react';
import { addTodo } from '../modules/todos';
const useAddTodo: () => Function = () => {
const dispatch = useDispatch();
return useCallback(text => dispatch(addTodo(text)), [dispatch]);
}
export default useAddTodo;
components/TodoInsert.tsx
import React, { ChangeEvent, FormEvent, useState }from 'react';
import useAddTodo from '../hooks/useAddTodo';
const TodoInsert: React.FC = () => {
const [value, setValue] = useState('');
const addTodo = useAddTodo();
const onChange= (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const onSubmit = (e:FormEvent) => {
e.preventDefault();
addTodo(value);
setValue('');
};
return (
<form onSubmit={onSubmit}>
<input
placeholder="Input todo item."
value={value}
onChange={onChange}
/>
<button type="submit">Add</button>
</form>
);
}
export default TodoInsert;
useTodoActions Hook
useTodoActions
Hook toggle/remove todo item.
hooks/useTodoActions.ts
import { useDispatch } from 'react-redux';
import { useCallback } from 'react';
import { toggleTodo, removeTodo } from '../modules/todos';
const useTodoActions: (id: number) => any = (id: number) => {
const dispatch = useDispatch();
const onToggle = useCallback(() => dispatch(toggleTodo(id)), [dispatch, id]);
const onRemove = useCallback(() => dispatch(removeTodo(id)), [dispatch, id]);
return { onToggle, onRemove };
}
export default useTodoActions
components/TodoItem.tsx
import React from 'react';
import './TodoItem.css';
import { Todo } from '../modules/todos';
import useTodoActions from '../hooks/useTodoActions';
type TodoItemProps = {
todo: Todo;
};
function TodoItem({ todo }: TodoItemProps) {
const { onToggle, onRemove } = useTodoActions(todo.id);
return (
<li className={`TodoItem ${todo.done ? 'done': ''}`}>
<span className='text' onClick={onToggle}>{todo.text}</span>
<span className='remove' onClick={onRemove} >(X)</span>
</li>
);
}
export default TodoItem;
ducks pattern
- Always
reducer()
function shouldexport default
- Always module’s action creator
export
as function - Always bring action type formed as
npm-module-orr-app/reducer/ACTION_TYPE
- Maybe action types
export
asUPPER_SNAKE_CASE
Source code
You can see the source codes which are used here https://github.com/semonec/react_redux_hooks_study
Comments