ToDo App created to present Redux best practices based on Redux Style Guide.
I've decided to forgo the use of:
- Redux Toolkit
- Immer
- feature folders file structure
- Reducers as State Machines
because I found this setup in projects much more frequently.
# Priority A Rules (Essential):
-
// todosReducer.ts case 'todos/set': return [...action.todos]
You can use redux-immutable-state-invariant to warn you against mutating state, or use Immer to allow mutable methods.
-
// todosReducer.ts case 'todos/toggle': return state.map((todo) => todo.id === action.id ? { ...todo, completed: !todo.completed } : todo )
No asynchronous logic and generating random values (
Date.now()
,Math.random()
) in reducers. -
// Incorrect case 'todos/set': return new Map()
Avoid putting Promises, Symbols, Maps/Sets, functions, or class instances into the Redux store state.
-
// reducers/index.ts const rootReducer = combineReducers({ todos: todosReducer, filter: filterReducer, }) // store.ts const store = createStore(rootReducer)
# Priority B Rules (Strongly Recommended):
-
RTK simplifies logic and promotes good practices.
-
Immer allows you to write simpler immutable updates using "mutative" logic.
-
// todosReducer.ts case 'todos/toggle': return state.map((todo) => todo.id === action.id ? { ...todo, completed: !todo.completed } : todo )
Prefer logic in reducers instead of click handlers, for example.
-
Use of static typing does make "spread return" safer and somewhat more acceptable.
-
// reducers/index.ts const rootReducer = combineReducers({ todos: todosReducer, filter: filterReducer, })
Avoid use of the word "reducer" in the key names.
-
"Root state slices should be defined and named based on the major data types or areas of functionality in your application, not based on which specific components you have in your UI."
-
"Trying to update a deeply nested field can become very ugly very fast."
-
"We recommend trying to treat actions more as "describing events that occurred", rather than "setters"."
-
"You should be able to read through a list of dispatched action types, and have a good understanding of what happened in the application."
-
// avoid this const action = () => { dispatch(deleteCompletedTodos()) dispatch(setFilter(Filters.SHOW_ACTIVE)) }
-
Not every value must be kept in redux. "Values that are "local" should generally be kept in the nearest UI component"
// TodoInput.tsx const [inputValue, setInputValue] = useState(INITIAL_INPUT_VALUE)
-
Prefer using
useSelector
anduseDispatch
as the default way to interact with a Redux store from your React components. -
Reading data at a more granular level typically leads to better UI performance, as fewer components will need to render when a given piece of state changes.
// TodoListItem.tsx const { title, completed } = useSelector(selectTodoById(id))
-
"Having selectors read smaller values means it is less likely that a given state change will cause this component to render."
// TodoList.tsx const allTodosIds = useSelector(selectAllTodosIds) const activeTodosIds = useSelector(selectActiveTodosIds) const completedTodosIds = useSelector(selectCompletedTodosIds) const activeFilter = useSelector(selectFilter)
-
"The type systems will catch many common mistakes, improve the documentation of your code, and ultimately lead to better long-term maintainability."
// store.ts export type RootState = ReturnType<typeof store.getState>
-
// store.ts import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction' const composeEnhancers = composeWithDevTools({}) const store = createStore(rootReducer, composeEnhancers())
-
"Prefer using plain JavaScript objects and arrays for your state tree."
# Priority C Rules (Recommended):
-
// todosReducer.ts interface ITodoAction { type: | 'todos/add' | 'todos/toggle' | 'todos/set' | 'todos/delete' | 'todos/reset' | 'todos/deleteCompleted' (...) }
-
{ type: string payload?: any error?: boolean meta?: any }
-
"Using action creators provides consistency, especially in cases where some kind of preparation or additional logic is needed to fill in the contents of the action (such as generating a unique ID)."
//todosReducer.ts export const addTodo = (title: string) => { const nextTodoId = Number( String(Date.now()) + String(Math.floor(Math.random() * Math.pow(10, 5))) ) return { type: 'todos/add', id: nextTodoId, title, } }
-
//todosReducer.ts export const fetchTodos = (): AppThunk => async (dispatch) => { fetch('https://jsonplaceholder.typicode.com/todos') .then((response) => response.json()) .then((todos: ITodo[]) => { return dispatch(setTodos(todos)) }) }
-
"We encourage moving complex synchronous or async logic outside components, usually into thunks. This is especially true if the logic needs to read from the store state.
However, the use of React hooks does make it somewhat easier to manage logic like data fetching directly inside a component, and this may replace the need for thunks in some cases."
-
//todosReducer.ts export const selectCompletedTodos = (rootState: RootState) => rootState.todos.filter(({ completed }) => completed) export const selectActiveTodos = (rootState: RootState) => rootState.todos.filter(({ completed }) => !completed)
-
// TodoList.tsx const activeTodos = useSelector(selectActiveTodos) const completedTodos = useSelector(selectCompletedTodos) const allTodos = useSelector(selectAllTodos) const activeFilter = useSelector(selectFilter)
-
"Connecting forms to Redux often involves dispatching actions on every single change event, which causes performance overhead and provides no real benefit."
// TodoInput.tsx const [inputValue, setInputValue] = useState(INITIAL_INPUT_VALUE)