Steinar Bangs blogg<p><strong>Convert react/redux webapp from saga/axios to RTK query and RTK listener</strong></p><p> <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">Redux saga</a> is a core component of my react/redux applications. Unfortuately <a href="https://www.reddit.com/r/reactjs/comments/1em8hc6/comment/lgy9pch/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1" rel="nofollow noopener" target="_blank">Redux Saga has been deprecated</a> and haven’t seen an upgrade in the last year. </p><p> This blog posts covers the replacement of <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">redux saga</a> and <a href="https://axios-http.com/" rel="nofollow noopener" target="_blank">axios</a> with <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a> and <a href="https://redux-toolkit.js.org/api/createListenerMiddleware" rel="nofollow noopener" target="_blank">RTK listener</a> in a react redux webapp. </p><p></p> <p><strong>Introduction</strong></p> <p> The structure of this blog post, is: </p><ol><li>First a walk through of how to use <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a> and <a href="https://redux-toolkit.js.org/api/createListenerMiddleware" rel="nofollow noopener" target="_blank">RTK listener</a> in a React application</li><li>Then I’ll sum up the changes made, what was good and what didn’t feel like an improvement to me</li></ol><p> That means if searching on the web for how to use <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a> landed you here, you’ll find the good stuff first and can skip the rest. </p><p> As for why you should use <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a>…? </p><p> If you, like me, is still using redux with <a href="https://axios-http.com/" rel="nofollow noopener" target="_blank">axios</a> and <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">saga</a>, then switching to <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a> is a no-brainer, since <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">redux Saga</a> hasn’t seen an update since February 1 2024, and <a href="https://www.reddit.com/r/reactjs/comments/1em8hc6/comment/lgy9pch/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1" rel="nofollow noopener" target="_blank">is now deprecated</a>. </p><p> If you’re not using redux because you have heard that it is hard to understand, bloated and require a lot of boilerplate, then you should reconsider. You should reconsider for the same reason I keep using redux: the existence of <a href="https://github.com/reduxjs/redux-devtools?tab=readme-ov-file#redux-devtools" rel="nofollow noopener" target="_blank">redux devtools</a>. </p><p> I find what I miss most when dealing with local state and context object is an easy way to examine the application’s current and entire state and trace when the state changes a particular value. </p><p> Writing console.log() to output the data to find what’s in there, is so… I don’t know… 1985…? </p><p> So without further ado, here’s how to use redux with <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a> in a react application (requires knowledge of JavaScript, it is helpful to know React, and knowledge of redux would help even more). </p> <p><strong>Using RTK query</strong></p> <p><strong>Adding the dependencies</strong></p> <p> Go to the directory containing package.json and run the following commands (first scrub existing dependencies and then add new ones): </p><p><em></em></p><pre>npm uninstall react react-dom react-redux "@reduxjs/toolkit"rm package-lock.jsonnpm install react react-dom react-redux "@reduxjs/toolkit"</pre><p> After this you should have all of the newest versions of the required dependencies. </p><p> If converting from <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">saga</a>/<a href="https://axios-http.com/" rel="nofollow noopener" target="_blank">axios</a>, then remember to scrub unneccessary dependencies after completing the conversion, with: </p><p><em></em></p><pre>npm uninstall redux-saga axiosrm package-lock.jsonnpm install</pre> <p><strong>Queries and mutations</strong></p> <p> With <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">redux-saga</a> and <a href="https://axios-http.com/" rel="nofollow noopener" target="_blank">axios</a>, data from a REST API backend is loaded by something like this </p> <em></em><p></p><pre><span>import</span> { takeLatest, call, put } from <span>'redux-saga/effects'</span>;<span>import</span> axios from <span>'axios'</span>;<span>import</span> { ACCOUNTS_REQUEST, ACCOUNTS_RECEIVE, ACCOUNTS_FAILURE } from <span>'../reduxactions'</span>;<span>export</span> <span>default</span> <span>function</span>* accountsSaga() { <span>yield</span> takeLatest(ACCOUNTS_REQUEST, receiveAccountsResult);}<span>function</span>* <span>receiveAccountsResult</span>() { <span>try</span> { <span>const</span> <span>response</span> = <span>yield</span> call(getAccounts); <span>const</span> <span>accountsresult</span> = (response.headers[<span>'content-type'</span>] === <span>'application/json'</span>) ? response.data : {}; <span>yield</span> put(ACCOUNTS_RECEIVE(accountsresult)); } <span>catch</span> (error) { <span>yield</span> put(ACCOUNTS_FAILURE(error)); }}<span>function</span> <span>getAccounts</span>() { <span>return</span> axios.get(<span>'/api/accounts'</span>);}</pre> <p> In addition to the above code a redux reducer to store the data in the frontend is needed, and the redux actions must be defined (not shown here, this blog post is about <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a> and not about <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">redux-saga</a> and <a href="https://axios-http.com/" rel="nofollow noopener" target="_blank">axios</a>). </p><p> In <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a>, the above example would be replaced with a one liner (this is what is called “a query” in <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a> terminology): </p> <em></em><p></p><pre>getAccounts: builder.query({ query: () => <span>'/accounts'</span> }),</pre> <p> In addtion to creating the code fetching the data, this one liner also results in redux storage of the fetched data, and redux actions for starting, successful completion and failure. </p><p> To use the results of data fetched by an <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a> in a React component, a hook created from the above one liner is used </p> <em></em><p></p><pre><span>export</span> <span>default</span> <span>function</span> Home() { <span>const</span> { data: accounts = [] } = useGetAccountsQuery(); <span>return</span> (<p>Number <span>of</span> accounts: ${accounts.length}</p>);}</pre> <p> When the component is rendered an HTTP request is started to fetch the data, and when the data arrives the component will be re-rendered. There is also considerable logic behind caching data and refetching that decides whether to reload the data or not when navigating in and out of the component. </p><p> My brief experience with RTK query is enough to tell me that RTK query’s reload logic works a lot better than my own home brewed reload logic. </p><p> One difference from the replaced <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">saga</a>/<a href="https://axios-http.com/" rel="nofollow noopener" target="_blank">axios</a> code, was that the reducer where the fetched data is stored, has no default value. So without the “= []” in the example above, accounts would be <code>undefined</code> on the first render. </p><p> Since the returned data is in the redux state, it is possible to use the useSelector() hook to access the data, but using a regular selector won’t trigger the fetch and reload logic. So as a rule of thumb: always start with the generated hook. </p><p> It is possible to add arguments to a query </p> <em></em><p></p><pre>getAccount: builder.query({ query: (username) => <span>'/account/'</span> + username }),</pre> <p> The query argument would then added to the hook used in the react component: </p> <em></em><p></p><pre><span>const</span> { data: account = {} } = useGetAccountQuery(<span>'steban'</span>);</pre> <p> Default for a query is to use HTTP GET, but using e.g. POST is simple and still an one-liner </p> <em></em><p></p><pre>postAccountAdminstatus: builder.query({ query: body => ({ url: <span>'/account/adminstatus'</span>, method: <span>'POST'</span>, body }) }),</pre> <p> The “body” argument needs to be possible to translate into JSON, to be sent as the POST body (and in my experience it is best to make the POSTed JSON a JSON object). </p> <em></em><p></p><pre><span>const</span> { data: <span>adminStatus</span> } = usePostAccountAdminstatusQuery({ username: <span>'steban'</span> });</pre> <p> Queries are for fetching data only. Even if a POST is used a query shouldn’t be used on an endpoint that modifies data on the backend. </p><p> To modify data you use a mutation, and a mutation can also be a one-liner </p> <em></em><p></p><pre>postAccountModify: builder.mutation({ query: body => ({ url: <span>'/account/modify'</span>, method: <span>'POST'</span>, body }) }),</pre> <p> The usage if the mutation hook is a little bit more complex than the query hook: </p> <em></em><p></p><pre><span>const</span> { data: account = {} } = useGetAccountQuery(<span>'steban'</span>);<span>const</span> [ <span>postAccountModify</span> ] = usePostAccountModifyMutation();<span>const</span> <span>onSaveButtonClicked</span> = <span>async</span> () => <span>await</span> postAccountModify(account);</pre> <p> I.e.: </p><ol><li>The mutation function is picked out of the hook return value</li><li>Then an async lambda is created using that function to post the return value</li><li>The account value is used in the form elements</li><li><p> The onSaveButtonClicked is used in </p> <em></em><p></p><pre><button onClick={onSaveButtonClicked}>Save account</button></pre> </li></ol><p> After updating data in the server, you would like the redux data updated with the new values. </p><p> The simplest way to get updated redux data, is to trigger a reload of the query that fetched the data in the first place. </p><p> To trigger a reload, put a tag on the query that should be reloaded: </p> <em></em><p></p><pre>getAccounts: builder.query({ query: () => <span>'/accounts'</span>, providesTags: [<span>'Accounts'</span>] }),</pre> <p> and then invalidate that tag after POSTing a mutation: </p> <em></em><p></p><pre>postAccountModify: builder.mutation({ query: body => ({ url: <span>'/account/modify'</span>, method: <span>'POST'</span>, body }), invalidatesTags: [<span>'Accounts'</span>],}),</pre> <p> The getAccounts query will be re-run and components with the <code>useGetAccountsQuery()</code> hook will be re-rendered. </p><p> That if the accounts had already been loaded by a hook in a react component, it isn’t neccessarily reloaded the next time the component is loaded, so without the invalidation, navigating to the component might show the old data. </p><p> But using invalidation of tags makes everything works smoothly and makes the data be updated on visit. </p><p> If the return from the mutation contains the the updated data, there is no need to make an invalidation. Then the redux storage of the query can be updated with the mutation return: </p> <em></em><p></p><pre>postAccountModify: builder.mutation({ query: body => ({ url: <span>'/account/modify'</span>, method: <span>'POST'</span>, body }), <span>async</span> onQueryStarted(body, { dispatch, queryFulfilled }) { <span>try</span> { <span>const</span> { data: <span>accountsAfterAccountModify</span> } = <span>await</span> queryFulfilled; dispatch(api.util.updateQueryData(<span>'getAccounts'</span>, <span>undefined</span>, () => accountsAfterAccountModify)); } <span>catch</span> {} },}),</pre> <p> The <code>await queryFulfilled</code> in <code>onQueryStarted()</code> waits for a successful mutation return and then then dispatches a redux action that replaces the results of a query </p><p> The tricky bit, when replacing query values in this way, is argument 2 of the <code>updateQueryData()</code> redux action creator, which is <code>undefined</code> in the above example. </p><p> If the query had an argument (e.g. the username), the replacement needs to have the same value. If not, there is no replacement and no error message. In this case the query has no argument, and then <code>undefined</code> is used. </p><p> The queries and mutations in my converted projects all live in a single file api.js on the top and all look a bit like this: </p> <em></em><p></p><pre><span>import</span> { createApi, fetchBaseQuery } from <span>'@reduxjs/toolkit/query/react'</span>;<span>export</span> <span>const</span> <span>api</span> = createApi({ reducerPath: <span>'api'</span>, baseQuery: (...args) => { <span>const</span> <span>api</span> = args[1]; <span>const</span> <span>basename</span> = api.getState().basename; <span>return</span> fetchBaseQuery({ baseUrl: basename + <span>'/api'</span> })(...args); }, endpoints: (builder) => ({ getAccounts: builder.query({ query: () => <span>'/accounts'</span> }), getAccount: builder.query({ query: (username) => <span>'/account/'</span> + username }), postAccountAdminstatus: builder.query({ query: body => ({ url: <span>'/account/adminstatus'</span>, method: <span>'POST'</span>, body }) }), postAccountModify: builder.mutation({ query: body => ({ url: <span>'/account/modify'</span>, method: <span>'POST'</span>, body }), <span>async</span> onQueryStarted(body, { dispatch, queryFulfilled }) { <span>try</span> { <span>const</span> { data: <span>accountsAfterAccountModify</span> } = <span>await</span> queryFulfilled; dispatch(api.util.updateQueryData(<span>'getAccounts'</span>, <span>undefined</span>, () => accountsAfterAccountModify)); } <span>catch</span> {} }, }), }),});<span>export</span> <span>const</span> { useGetAccountsQuery, useCetAccountQuery, usePostAccountAdminstatusQuery, usePostAccountModifyMutation,} = api;</pre> <p> I.e. </p><ol><li>The <code>reducerPath</code> is the top level path of the api’s reducer slice in the redux state</li><li>The <code>baseQuery</code> sets the base path prefix for all API calls, in this case it is the value of ‘/api’ prefixed by redux state value <code>basename</code></li><li>The <code>endpoints</code> contains the specification for the queries and mutations</li><li>All mutations are exported at the end for ease of use in the</li></ol><p> For those who like looking at example code, here is the “api.js” of some React web applications converted from <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">saga</a>/<a href="https://axios-http.com/" rel="nofollow noopener" target="_blank">axios</a> to <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a>: </p>applicationdescription<a href="https://github.com/steinarb/handlereg/blob/b038efc51fcbb77f563bb9babe8253bb14a03223/handlereg.web.frontend/src/main/frontend/src/api.js" rel="nofollow noopener" target="_blank">handlereg</a>A groceries tracker and statistics app<a href="https://github.com/steinarb/sampleapp/blob/a77c5f038d6a0bd2a460685c71d77299ca881ba4/sampleapp.web.frontend/src/main/frontend/src/api.js" rel="nofollow noopener" target="_blank">sampleapp</a>A template application for React web applications with jersey backend and PostgreSQL database set up with liquibase<a href="https://github.com/steinarb/oldalbum/blob/4714edeacfc7210c07053a66c90d03ae2ef2901b/oldalbum.web.frontend/src/main/frontend/src/api.js" rel="nofollow noopener" target="_blank">oldalbum</a>An app wrapping 90-ies albums in 202x web technology<a href="https://github.com/steinarb/ukelonn/blob/77a86ac71af50f1948e8d0558d88c8e45a5f5790/ukelonn.web.frontend/src/main/frontend/api.js" rel="nofollow noopener" target="_blank">ukelonn</a>A weekly allowance app<a href="https://github.com/steinarb/authservice/blob/132a1242728bf8c251a36cb74492349eb0bbbd90/authservice/authservice.web.users.frontend/src/main/frontend/src/api.js" rel="nofollow noopener" target="_blank">authservice</a>A simple user, role and permission manager for <a href="https://shiro.apache.org/" rel="nofollow noopener" target="_blank">Apache Shiro</a> <p><strong>Adding RTK query to the redux store</strong></p> <p> The api object needs to be added to the reducer: </p> <em></em><p></p><pre><span>import</span> { combineReducers } from <span>'redux'</span>;<span>import</span> { createReducer } from <span>'@reduxjs/toolkit'</span>;<span>import</span> { api } from <span>'../api'</span>;<span>export</span> <span>default</span> (basename) => combineReducers({ [api.reducerPath]: api.reducer, basename: createReducer(basename, (builder) => builder),});</pre> <p> The exported function is a reducer creator that can be used to create a redux store state with two values: </p><ol><li>api which is a redux slice containing all state related to network communication and dowloaded results</li><li>basename which is a string value holding the react application’s basepath</li></ol><p> The creator function can be used to create a redux store: </p> <em></em><p></p><pre><span>import</span> rootReducer from <span>'./reducers'</span>;<span>import</span> { api } from <span>'./api'</span>;<span>import</span> listeners from <span>'./listeners'</span>;<span>// </span><span>Calculate the basename based on the URL of the vite assets directory</span><span>const</span> <span>baseUrl</span> = Array.from(document.scripts).map(s => s.src).filter(src => src.includes(<span>'assets/'</span>))[0].replace(<span>/\/assets\/.*/</span>, <span>''</span>);<span>const</span> <span>basename</span> = <span>new</span> <span>URL</span>(baseUrl).pathname;<span>const</span> <span>store</span> = configureStore({ reducer: rootReducer(basename), middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware).prepend(listeners.middleware),});</pre> <p> First we calculate the application’s basepath from the vite artifact the code is loaded from, then a store is created with a reducer consisting of api and basename, and then two middlewares are added: api and listener </p><p> “What is listeners?” you ask at this point. </p><p> I am glad you asked! Check out the next section. </p><p> For those who like looking at example code, some example redux store setups for <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a> <a href="https://redux-toolkit.js.org/api/createListenerMiddleware" rel="nofollow noopener" target="_blank">and RTK listener</a>: </p>appreducersredux store setuphandlereg<a href="https://github.com/steinarb/handlereg/blob/b038efc51fcbb77f563bb9babe8253bb14a03223/handlereg.web.frontend/src/main/frontend/src/reducers/index.js" rel="nofollow noopener" target="_blank">reducers/index.js</a><a href="https://github.com/steinarb/handlereg/blob/b038efc51fcbb77f563bb9babe8253bb14a03223/handlereg.web.frontend/src/main/frontend/src/index.js#L22" rel="nofollow noopener" target="_blank">top index.js</a>sampleapp<a href="https://github.com/steinarb/sampleapp/blob/a77c5f038d6a0bd2a460685c71d77299ca881ba4/sampleapp.web.frontend/src/main/frontend/src/reducers/index.js" rel="nofollow noopener" target="_blank">reducers/index.js</a><a href="https://github.com/steinarb/sampleapp/blob/a77c5f038d6a0bd2a460685c71d77299ca881ba4/sampleapp.web.frontend/src/main/frontend/src/index.js#L22" rel="nofollow noopener" target="_blank">top index.js</a>oldalbum<a href="https://github.com/steinarb/oldalbum/blob/4714edeacfc7210c07053a66c90d03ae2ef2901b/oldalbum.web.frontend/src/main/frontend/src/reducers/index.js" rel="nofollow noopener" target="_blank">reducers/index.js</a><a href="https://github.com/steinarb/oldalbum/blob/4714edeacfc7210c07053a66c90d03ae2ef2901b/oldalbum.web.frontend/src/main/frontend/src/index.js#L21" rel="nofollow noopener" target="_blank">top index.js</a>ukelonn<a href="https://github.com/steinarb/ukelonn/blob/77a86ac71af50f1948e8d0558d88c8e45a5f5790/ukelonn.web.frontend/src/main/frontend/reducers/index.js#L39" rel="nofollow noopener" target="_blank">reducers/index.js</a><a href="https://github.com/steinarb/ukelonn/blob/77a86ac71af50f1948e8d0558d88c8e45a5f5790/ukelonn.web.frontend/src/main/frontend/index.js#L22" rel="nofollow noopener" target="_blank">top index.js</a>authservice<a href="https://github.com/steinarb/authservice/blob/7e3e2911a888b7f45561b13fd1e3c09d15deacc7/authservice/authservice.web.users.frontend/src/main/frontend/src/reducers/index.js#L25" rel="nofollow noopener" target="_blank">reducers/index.js</a><a href="https://github.com/steinarb/authservice/blob/7e3e2911a888b7f45561b13fd1e3c09d15deacc7/authservice/authservice.web.users.frontend/src/main/frontend/src/index.js#L17" rel="nofollow noopener" target="_blank">top index.js</a> <p><strong>Using RTK listener to perform actions on REDUX actions</strong></p> <p> <a href="https://redux-toolkit.js.org/api/createListenerMiddleware" rel="nofollow noopener" target="_blank">Listener</a> fills the same role as another useful <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">saga</a> role: the ability to listen for redux actions and act on them: </p> <em></em><p></p><pre><span>import</span> { createListenerMiddleware } from <span>'@reduxjs/toolkit'</span>;<span>import</span> { isFailedRequest } from <span>'./matchers'</span>;<span>const</span> <span>listeners</span> = createListenerMiddleware();listeners.startListening({ matcher: isFailedRequest, effect: ({ payload }) => { <span>const</span> { originalStatus } = payload || {}; <span>const</span> <span>statusCode</span> = parseInt(originalStatus); <span>if</span> (statusCode === 401 || statusCode === 403) { location.reload(<span>true</span>); <span>// </span><span>Will return to current location after the login process</span> } }})<span>export</span> <span>default</span> listeners;</pre> <p> This listener listens for failed <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a> HTTP calls to the API and checks the status codes for 401 “Needs Authentication” and 403 “Not Authorized” and reloads the app with the current URL to let apache shiro deal with it (refresh the auth and just open this page, pop up login or redirect to unauthorized page). </p><p> This is “matchers.js” where <code>isFailedRequest</code> is found: </p> <em></em><p></p><pre><span>import</span> { isAnyOf } from <span>'@reduxjs/toolkit'</span>;<span>import</span> { api } from <span>'./api'</span>;<span>export</span> <span>const</span> <span>isFailedRequest</span> = isAnyOf( api.endpoints.getAccounts.matchRejected, api.endpoints.getAccount.matchRejected, api.endpoints.postAccountAdminstatus.matchRejected, api.endpoints.postAccountModify.matchRejected,);</pre> <p> For those who prefer looking at example code, example listener.js and matcher.js files can be found here: </p>applisteners definitionmatchershandlereg<a href="https://github.com/steinarb/handlereg/blob/b038efc51fcbb77f563bb9babe8253bb14a03223/handlereg.web.frontend/src/main/frontend/src/listeners.js" rel="nofollow noopener" target="_blank">listeners.js</a> sampleapp<a href="https://github.com/steinarb/sampleapp/blob/a77c5f038d6a0bd2a460685c71d77299ca881ba4/sampleapp.web.frontend/src/main/frontend/src/listeners.js" rel="nofollow noopener" target="_blank">listeners.js</a> oldalbum<a href="https://github.com/steinarb/oldalbum/blob/4714edeacfc7210c07053a66c90d03ae2ef2901b/oldalbum.web.frontend/src/main/frontend/src/listeners.js#L34" rel="nofollow noopener" target="_blank">listeners.js</a> ukelonn<a href="https://github.com/steinarb/ukelonn/blob/77a86ac71af50f1948e8d0558d88c8e45a5f5790/ukelonn.web.frontend/src/main/frontend/listeners.js#L16" rel="nofollow noopener" target="_blank">listeners.js</a><a href="https://github.com/steinarb/ukelonn/blob/77a86ac71af50f1948e8d0558d88c8e45a5f5790/ukelonn.web.frontend/src/main/frontend/matchers.js" rel="nofollow noopener" target="_blank">matchers.js</a>authservice<a href="https://github.com/steinarb/authservice/blob/7e3e2911a888b7f45561b13fd1e3c09d15deacc7/authservice/authservice.web.users.frontend/src/main/frontend/src/listeners.js" rel="nofollow noopener" target="_blank">listeners.js</a><a href="https://github.com/steinarb/authservice/blob/7e3e2911a888b7f45561b13fd1e3c09d15deacc7/authservice/authservice.web.users.frontend/src/main/frontend/src/matchers.js" rel="nofollow noopener" target="_blank">matchers.js</a> <p><strong>Using RTK queries’ successful loads in other reducers</strong></p> <p> It is possible to use the successful completion of a mutation to clear reducers backing forms: </p> <em></em><p></p><pre><span>import</span> { createReducer } from <span>'@reduxjs/toolkit'</span>;<span>import</span> { SELECT_STORE } from <span>'../actiontypes'</span>;<span>import</span> { api } from <span>'../api'</span>;<span>const</span> <span>defaultState</span> = { storeId: -1, storename: <span>''</span>, gruppe: 2,};<span>export</span> <span>default</span> <span>const</span> <span>storeReducer</span> = createReducer(defaultState, builder => { builder .addCase(SELECT_STORE, (state, action) => action.payload) .addMatcher(api.endpoints.postNewstore.matchFulfilled, () => ({ ...defaultState })) .addMatcher(api.endpoints.postModifystore.matchFulfilled, () => ({ ...defaultState }));});</pre> <p> Here both the completion of a successful postNewstore mutation and a successful postModifystore mutation will set the reducer to its default state (and clear the forms backed by the redux state value). </p><p> Note the use of <code>addMatcher()</code> rather than <code>addCase()</code> to matche the actions. </p><p> All <code>matchFulfilled</code> actions have the same redux type, so traditional redux type matching won’t work. But the matchers provided by the api object can be used instead. </p> <p><strong>Dependent queries</strong></p> <p> A common requirement is that one REST call needs the value of another value before being called. For this “dependent queries” in RTK query can be used: </p> <em></em><p></p><pre><span>export</span> <span>default</span> <span>function</span> Home() { <span>const</span> { data: overview = {}, isSuccess: overviewIsSuccess } = useGetOverviewQuery(); <span>const</span> { data: purchases = [] } = useGetPurchasesQuery(overview.accountid, { skip: !overviewIsSuccess }); ...</pre> <p> The above example the getPurchases query needs an account id that can be found in the results of the getOverview query. </p><p> The <a href="https://redux-toolkit.js.org/rtk-query/usage/queries#query-hook-options" rel="nofollow noopener" target="_blank">“skip” property</a> of the query option object to <code>useGetPurchasesQuery()</code> prevents <code>useGetPurchasesQuery()</code> from being executed until <code>useGetOverviewQuery()</code> has completed successfully. </p> <p><strong>Converting apps</strong></p> <p> For those preferring to look at code, here are the diffs from converting from <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">redux</a>/<a href="https://axios-http.com/" rel="nofollow noopener" target="_blank">axios</a> to <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK queries</a> and <a href="https://redux-toolkit.js.org/api/createListenerMiddleware" rel="nofollow noopener" target="_blank">RTK listeners</a>: </p>appDescription<a href="https://github.com/steinarb/handlereg/commit/6594b9c12072655286758192a57203a73f31b072" rel="nofollow noopener" target="_blank">handlereg</a>A groceries tracker and statistics app<a href="https://github.com/steinarb/sampleapp/commit/a77c5f038d6a0bd2a460685c71d77299ca881ba4" rel="nofollow noopener" target="_blank">sampleapp</a>A template application for React web applications with jersey backend and PostgreSQL database set up with liquibase<a href="https://github.com/steinarb/oldalbum/commit/568d803ad87ff37253ae0dca370c81b1f30615af" rel="nofollow noopener" target="_blank">oldalbum</a>An app wrapping 90-ies albums in 202x web technology<a href="https://github.com/steinarb/ukelonn/commit/2aba9a7103b0f632cc24a693840faa2d28c40dbf" rel="nofollow noopener" target="_blank">ukelonn</a>A weekly allowance app<a href="https://github.com/steinarb/authservice/commit/cb8473ffd9d0a138e2f6428d61339cdf9f9cc1f2" rel="nofollow noopener" target="_blank">authservice</a>A simple user, role and permission manager for <a href="https://shiro.apache.org/" rel="nofollow noopener" target="_blank">Apache Shiro</a><p> Reduction of JavaScript source code size for the various apps (source code line numbers from <a href="https://github.com/AlDanial/cloc?tab=readme-ov-file#cloc" rel="nofollow noopener" target="_blank">cloc</a> run before and after the conversion): </p>appsize reduction in %size reduction in <a rel="nofollow noopener" class="hashtag u-tag u-category" href="https://steinar.bang.priv.no/tag/lines/" target="_blank">#lines</a>handlereg34%674sampleapp32%359oldalbum14%608ukelonn25%1211authservice31%841<p> Summing up the numbers from doing the conversion </p><ol><li>A 14% to 35% reduction of JavaScript code of the apps (with lots of boilerplate gone). The package with lowest percentage has a number of lines reduction on par with the rest of the “real” applications</li><li>No noticable difference in dependencies size even though I was already using RTK and got rid of <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">redux-saga</a> and <a href="https://axios-http.com/" rel="nofollow noopener" target="_blank">axios</a></li><li>No significant difference in vite build artifact size (even an <i>increase</i> in one case) (numbers can be provided on request)</li></ol><p> As said at the start, the obsoletion of <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">redux-saga</a> made doing this change a non-brainer for me. </p><p> But losing all of the boiler plate around network requests made the applications easier to read and understand and maintain. And the logic around lazy loading, reloads and caching of data is a lot better than my home-brewed redux-router powered logic. </p><p> I’m not opposed to learning new stuff, but I can be quite reluctant to let go of stuff I like, and I was really happy about <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">redux saga</a> as outlined in <a href="http://steinar.bang.priv.no/?p=556" rel="nofollow noopener" target="_blank">Yep, I’m still using redux</a>. </p><p> So did I lose anything in this conversion? </p><p> Downsides are: </p><ol><li>Harder to examine data loaded by <a href="https://redux-toolkit.js.org/rtk-query/overview" rel="nofollow noopener" target="_blank">RTK query</a> in the <a href="https://github.com/reduxjs/redux-devtools?tab=readme-ov-file#redux-devtools" rel="nofollow noopener" target="_blank">redux devtools</a> (but still possible)</li><li>More complex react components:<ol><li><p> prior this change, redux served up data ready for consumption in the components and with defaults, i.e. this </p> <em></em><p></p><pre><span>const</span> <span>butikker</span> = useSelector(state => state.butikker);</pre> <p> is simpler than this: </p> <em></em><p></p><pre><span>import</span> { useGetButikkerQuery } from <span>'../services/butikker'</span>;... <span>const</span> { data: butikker = [] } = useGetButikkerQuery();</pre> </li><li>Prior to this change, gathering the data for a save to the back end took place in a <a href="https://redux-saga.js.org/" rel="nofollow noopener" target="_blank">saga</a>, so there was no need to clutter up the component with data not used in the actual render</li></ol></li><li>It’s kinda hard (but not impossible) to use the loaded data as backing storage for the edit form, and then save back from the data. The assumption is that one is to use something like useState() for this, but I don’t wish to do that</li></ol><p> But putting stuff back in the react components makes fewer places to look for stuff and makes my react component look more similar to others’ react components, so maybe this isn’t so bad after all…? </p> <p><strong>Where to go next?</strong></p> <p> I startet using <a href="https://redux-toolkit.js.org/" rel="nofollow noopener" target="_blank">Redux ToolKit (RTK)</a> back in 2019. I quickly figured that <a href="https://redux-toolkit.js.org/api/createReducer" rel="nofollow noopener" target="_blank">createReducer()</a> and <a href="https://redux-toolkit.js.org/api/createAction" rel="nofollow noopener" target="_blank">createAction()</a> could make my existing reducers and redux action creation more robust. </p><p> I tried using <a href="https://redux-toolkit.js.org/api/createSlice" rel="nofollow noopener" target="_blank">redux slices</a> at that point in time, but since I had to mix the autocreated redux action with manually created redux actions to trigger REST API operations and handle REST API errors, I figured that redux slices was more work than useful. </p><p> And I later also landed on the “flattened redux state approach” outlined in <a href="http://steinar.bang.priv.no/?p=556" rel="nofollow noopener" target="_blank">Yep, I’m still using redux</a> (see that article for the reasoning) that didn’t fit well with slices, so I dropped it back then. </p><p> But with RTK query I no longer have to manually create actions to start API operations or handle errors, since these are created when creating queries or mutations, so maybe it is time to give redux slices a new try…? </p> <p></p><p><a rel="nofollow noopener" class="hashtag u-tag u-category" href="https://steinar.bang.priv.no/tag/css/" target="_blank">#css</a> <a rel="nofollow noopener" class="hashtag u-tag u-category" href="https://steinar.bang.priv.no/tag/dom/" target="_blank">#dom</a> <a rel="nofollow noopener" class="hashtag u-tag u-category" href="https://steinar.bang.priv.no/tag/html/" target="_blank">#html</a> <a rel="nofollow noopener" class="hashtag u-tag u-category" href="https://steinar.bang.priv.no/tag/lines/" target="_blank">#lines</a> <a rel="nofollow noopener" class="hashtag u-tag u-category" href="https://steinar.bang.priv.no/tag/react/" target="_blank">#react</a> <a rel="nofollow noopener" class="hashtag u-tag u-category" href="https://steinar.bang.priv.no/tag/reactjs/" target="_blank">#reactjs</a> <a rel="nofollow noopener" class="hashtag u-tag u-category" href="https://steinar.bang.priv.no/tag/redux/" target="_blank">#redux</a> <a rel="nofollow noopener" class="hashtag u-tag u-category" href="https://steinar.bang.priv.no/tag/redux_saga/" target="_blank">#reduxSaga</a> <a rel="nofollow noopener" class="hashtag u-tag u-category" href="https://steinar.bang.priv.no/tag/redux_toolkit/" target="_blank">#reduxToolkit</a> <a rel="nofollow noopener" class="hashtag u-tag u-category" href="https://steinar.bang.priv.no/tag/saga/" target="_blank">#saga</a></p>