React Redux store example with slice
Older way of using Redux:
- separate file for reducer
- separate file for actions
Newer way:
- In one file called “slice” there are both: actions and reducers
So let’s see simple and easy example:
Project organization:
- Folder: “app” -> file “store.ts” and “hooks.ts”
file “store.ts”:
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import taskReducer from '../features/tasks/taskSlice';
export const store = configureStore({
reducer: {
tasks: taskReducer,
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
- in reducer object We can define many stores (reducers), each from separate slice
file “hooks.ts”:
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
OK, now each store will be in folder “features”, in subfolder, for exampe in “tasks: will be “taskSlice”:
- file “taskSlice.ts”
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState } from '../../app/store';
export interface TaskItem {
id: number,
name: string,
}
const initialState: TaskItem[] = [{id:1,name:"Wojtek"}]
export const taskSlice = createSlice({
name: 'tasks',
initialState,
reducers: {
addTask: (state, action: PayloadAction<{id:number,name:string}>) => {
const newTask: TaskItem = {id:action.payload.id, name: action.payload.name}
state.push(newTask)
},
deleteTask: (state, action: PayloadAction<{id:number}>) => {
return state = state.filter(task => task.id!==action.payload.id)
},
editTask: (state, action: PayloadAction<{id:number,name:string}>) => {
state.forEach(task=>{
if (task.id===action.payload.id){
task.name=action.payload.name
}
})
},
},
});
export const { addTask, deleteTask, editTask } = taskSlice.actions;
export const selectTasks = (state: RootState) => state.tasks;
export default taskSlice.reducer;
OK, now We can use it in Tasks.tsx component (rendering tasks):
- file “Tasks.tsx”:
import React, { useState } from 'react';
import { useAppSelector, useAppDispatch } from '../../app/hooks';
import {
addTask,
editTask,
deleteTask,
selectTasks,
TaskItem,
} from './taskSlice';
import styles from './Tasks.module.css';
export function Tasks() {
const tasks = useAppSelector(selectTasks);
const [selectedTaskId, setSelectedTaskId] = useState(0);
const [taskName, setTaskName] = useState('');
const dispatch = useAppDispatch();
const tasksList = tasks.map(task =>
(<li key={task.id} onClick={() => taskClickHandler(task)}>{task.id} : {task.name}</li>)
)
const getMaxId = () => {
if (tasks.length>0){
return Math.max(...tasks.map(task => task.id));
} else
{
return 0
}
}
const taskClickHandler = (task: TaskItem) => {
setSelectedTaskId(task.id)
setTaskName(task.name)
}
const addClickHandler = (taskName: string) => {
let currentMaxId: number = getMaxId()
dispatch(addTask({ id:currentMaxId+1, name: taskName }))
}
return (
<div>
<div className={styles.row}>
<input className={styles.textbox}
aria-label="Task id"
value={selectedTaskId}
onChange={() => { }} />
<input className={styles.textbox_long}
aria-label="Task name"
value={taskName}
onChange={(e) => setTaskName(e.target.value)} />
<button
className={styles.button}
aria-label="Add task"
onClick={() => addClickHandler(taskName)}
>
Add
</button>
<button
className={styles.button}
aria-label="Edit task"
onClick={() => dispatch(editTask({ id: selectedTaskId, name: taskName }))}
>
Edit
</button>
<button
className={styles.button}
aria-label="Delete task"
onClick={() => dispatch(deleteTask({ id: selectedTaskId }))}
>
Delete
</button>
<ul >{tasksList}</ul>
</div>
</div>
);
}
Notes: We have to also add store provider, the best place is index.tsx:
- index.tsx file:
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
import './index.css';
import 'bootstrap/dist/css/bootstrap.min.css';
const container = document.getElementById('root')!;
const root = createRoot(container);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
That’s all!