React Redux
Setup steps
- Create a new React application
vite create my-app --template react-ts
- Install dependencies
npm install @reduxjs/toolkit react-redux
- Create a new slice: Part of your application state, and it typically contains reducers, actions, and selectors
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
count: 0,
};
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.count += 1;
},
decrement(state) {
state.count -= 1;
},
},
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
- Create a store: A single object that holds the entire state of your application
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export default store;
- Wrap your app with the Provider component:
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import Counter from './Counter';
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
export default App;
- Connect your component to the store: To connect your component to the store, use the useSelector hook to extract the state from the store and the useDispatch hook to dispatch actions to update the state.
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.count);
const dispatch = useDispatch();
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
);
}
export default Counter;
7, Async: Adding async logic using createAsyncThunk: createAsyncThunk is a helper function provided by Redux RTK that makes it easy to handle asynchronous logic in your Redux store. It returns a thunk that dispatches pending, fulfilled, and rejected actions, based on the status of the async operation.
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
interface User {
id: number;
name: string;
}
interface UserState {
loading: boolean;
data: User[];
error: string | null;
}
const initialState: UserState = {
loading: false,
data: [],
error: null,
};
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
return data;
});
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUsers.pending, (state) => {
state.loading = true;
state.error = null;
});
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
});
builder.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message ?? 'Unknown error';
});
},
});
export default usersSlice.reducer;
- Using createSelector to compute derived data: createSelector is a helper function provided by Redux that allows you to compute derived data from a Redux store. It takes one or more input selectors and a transform function, and returns a memoized selector that returns the result of the transform function.
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from '../store';
import { User } from './usersSlice';
export const selectUsers = (state: RootState) => state.users.data;
export const selectAdminUsers = createSelector(selectUsers, (users: User[]) => {
return users.filter((user) => user.isAdmin);
});
- Using middleware: Redux RTK allows you to easily add middleware to your store, which can be used for things like logging, handling side effects, or modifying actions. Here's an example of how you can add middleware:
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import loggerMiddleware from './loggerMiddleware';
import rootReducer from './rootReducer';
const middleware = [...getDefaultMiddleware(), loggerMiddleware];
const store = configureStore({
reducer: rootReducer,
middleware,
});
export default store;
- Using createEntityAdapter for managing normalized data: createEntityAdapter is a helper function provided by Redux RTK that makes it easy to manage normalized data in your Redux store.
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
interface Todo {
id: number;
title: string;
completed: boolean;
}
interface TodosState {
ids: number[];
entities: Record<number, Todo>;
}
const todosAdapter = createEntityAdapter<Todo>();
const initialState: TodosState = todosAdapter.getInitialState();
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: todosAdapter.addOne,
removeTodo: todosAdapter.removeOne,
updateTodo: todosAdapter.updateOne,
},
});
export const { addTodo, removeTodo, updateTodo } = todosSlice.actions;
export const {
selectAll: selectAllTodos,
selectById: selectTodoById,
selectIds: selectTodoIds,
} = todosAdapter.getSelectors((state: RootState) => state.todos);
export default todosSlice.reducer;
Define a Todo interface to represent the shape of our data, and a TodosState interface to represent the shape of our slice's state.
Create a todosAdapter using the createEntityAdapter function
Initialize our slice's state using the getInitialState method of the adapter.
Define our slice's reducers using the addOne, removeOne, and updateOne methods of the adapter, which are shorthand methods for updating the state when adding, removing, or updating a single entity.
Finally, we export selectors using the getSelectors method of the adapter, which creates a set of memoized selectors based on the normalized data in our store.
Use these selectors to retrieve the data from our store in a structured way.
This approach can make it easier to work with normalized data in your Redux store, and can help you avoid unnecessary loops and lookups when working with complex data structures.
RTK Query with the basic store slice
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query';
import { createSlice } from '@reduxjs/toolkit';
// Define an API slice using RTK Query
const counterApiSlice = createApi({
reducerPath: 'counterApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
endpoints: (builder) => ({
getCount: builder.query<number, void>({
query: () => '/count',
}),
updateCount: builder.mutation<number, number>({
query: (count) => ({
url: '/count',
method: 'PUT',
body: { count },
}),
}),
}),
});
// Extract the generated actions and hooks
const { useGetCountQuery, useUpdateCountMutation } = counterApiSlice;
// Define the counter slice as before
const initialState = {
count: 0,
};
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.count += 1;
},
decrement(state) {
state.count -= 1;
},
},
});
// Add async actions that use the generated RTK Query hooks
export const { increment, decrement } = counterSlice.actions;
export const fetchCount = () => async (dispatch) => {
const { data: count } = await useGetCountQuery().unwrap();
dispatch(setCount(count));
};
export const updateCount = (count: number) => async (dispatch) => {
const { data: newCount } = await useUpdateCountMutation(count).unwrap();
dispatch(setCount(newCount));
};
// Export the reducer, including the generated API reducer
export default counterSlice.reducer;
export const counterApiReducer = counterApiSlice.reducer;