Stepping up your Redux game with Redux Toolkit and TypeScript
A brief overview of some cool features on Redux Toolkit that will boost code quality on your next React project
Do you often find yourself modifying half a dozen different files just to add a new Redux action? Maintaining gigantic reducer functions that keep getting bigger and bigger? Or maybe wasting hours debugging faulty reducers that mutate deeply nested state objects? Don’t get me started on integrating asynchronous calls with the store.
From the official Redux documentation, Redux Toolkit is the “official, opinionated, batteries-included toolset for efficient Redux development. It is intended to be the standard way to write Redux logic, and we strongly recommend that you use it.”
Redux Toolkit can help us to speed up development and avoid mistakes, with a set of tools dedicated to store setup and configuration, reducer and action creation, selecting data from the store, or performing operations on a server. RTK includes some libraries like immer, reselect and redux-thunk, so we can use them right away. It is also written and designed to enable seamless integration with TypeScript applications.
Redux Toolkit: createSlice
To simplify the tedious process of writing reducers, action types and action creators, Redux Toolkit has a function that will auto-generate the types and creators we need from the reducer functions defined. Here’s how we can set up a basic slice that maintains a value in the global state.
name
will be used as a prefix on every action for this slice. When dispatched, setDate
would have the shape: {type: “dateSlice/setDate”, payload: ‘2021–10–17"}
.
initialState
is, well, the initial state of the reducer.
reducers
is an object of ‘case reducer’ functions. Each one will generate an action (with the associated action type) that we can dispatch. Think of these functions as the case statements (or switch blocks) we would write on a regular Redux reducer function. Notice how we are not returning a new state on each ‘case reducer’ function, but rather mutating the state object directly. That is because createSlice
passes the reducer object to createReducer
, which uses immer
internally to safely mutate a proxy state.
We can then use react-redux
utilities useSelector
and useDispatch
to read the state variables on the slice and dispatch actions to the store.
import { useDispatch, useSelector } from "react-redux";
import { selectDate, setDate } from 'store/dateSlice'function MyComponent(){
const date = useSelector(selectDate);
const dispatch = useDispatch(); function setDate(newDate: string){
dispatch(setDate(newDate));
} // ...
}
Here’s how we would write the same Redux logic without RTK. This might probably be ok for a couple of actions, but adding a new action type, action creator and reducer case function for each new interaction with the store can quickly become a tiresome task. The process is very error-prone, and we end up with fragile code that’s difficult to debug and maintain.
Redux Toolkit Query: createApi
Fetching data from a server, caching it, making updates to that data and handling loading states or errors can be a heavy coding task, especially if we want to integrate it all with a Redux store (more often than not).
Here’s how we would perform asynchronous calls with regular Redux actions and reducers. We are responsible for keeping and updating loading and error state variables in the store, adding new fetching
, success
or failure
case reducers, and writing the corresponding action types and creators as well. We also need to set up a middleware like redux-thunk
to write action creators that return functions instead of actions.
This is a lot of code for a simple server request, and the actual data fetching code is not even included on this code snippet. If you’re familiar with performing asynchronous calls while maintaining a Redux store, you already know this gets more complex as we keep adding new server calls. There must be another way…
RTK Query is a very powerful add-on included in the Redux Toolkit package that simplifies data fetching and caching logic. Built on top of createSlice
and createAsyncThunk
, RTK Query can track loading states, avoid duplicate requests and cache fetched data. No need to write any action types, action creators or reducer functions; these are all auto-generated for us.
At the core of RTK Query iscreateApi
. With a few lines of code we can define the endpoints needed to fetch or update data, cache the results and handle loading, fetching or error states. Here’s how we can set up a basic API to fetch and update elements from a server.
reducerPath
is a unique key that will be used to mount the API service on the store and give us access to returned data or any fetching-related flags we may need.
baseQuery
is the base query handler for each endpoint. We can use fetchBaseQuery
, a lightweight fetch wrapper also shipped with RTK Query, to handle all request headers and response parsing for us.
endpoints
is where we define the set of operations we will perform on the server. These can be either queries or mutations. Queries are typically requests to retrieve data from the server, while mutations consist of any updates on the server.
tagTypes
is an array of tags we can define to handle caching and invalidation of fetched data. A tag can be represented either as a plain string name or as an object with the shape {tag: string, id?: string|number}
. We can attach (or provide) a tag to the data returned from a query and invalidate it on mutations. By invalidating cached data, it can be automatically refetched or removed. Notice on the code snippet above the use of provideTags
and invalidatesTags
on the getElementsByDate
and updateElement
endpoint definitions. If we wish for more granular control over the cached data, we can choose to pass a function to these arguments instead of an array. Take a look at the code below, where we go for more specificity on the caching and invalidation mechanisms, attaching an id
to each element cached.
Redux Toolkit Query: generated hooks
One of my favorite features of RTK query is the auto-generated hooks exported from the API slice to perform queries from any functional component. Each query hook returns an object with the returned result, error messages, loading/fetching states, flags for a successful, failed or uninitialized request, and a refetch function that we can use to force a new query. We can have extra control over the query, passing some parameters to the hook like skip
to avoid unnecessary queries or pollingInterval
to trigger a refetch on the interval provided.
For mutations, the auto-generated hook returns a tuple with a trigger function and an object with status
, error
and data
values. Unlike queries, mutations don’t execute automatically, we have to execute the trigger function returned from the hook. Take a look at the following example on how to use the query and mutation hooks exported from createApi
.
What’s next?
These are just a few highlights of Redux Toolkit, a powerful toolset I encourage you to try on your next frontend project. You should also check the configureStore
utility, which simplifies the store set up and configuration with middleware and enhancers; the extraReducers
parameter oncreateSlice
to respond to other action types besides the ones the slice has generated (this could be particularly useful if we want to locally save and update data fetched from a createApi
API); and dig deeper on the createAPI
parameters and available customization (like skip
option for conditional fetching, polling through pollingInterval
, and streaming updates using the onCacheEntryAdded
option).