Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

It would be nice if there was a way to operate on the redux cache of an RTK query as if it was a redux slice #4849

Open
steinarb opened this issue Feb 10, 2025 · 12 comments

Comments

@steinarb
Copy link

It would be nice if there was a way to operate on the redux cache of an RTK query as if it was a redux slice.

My use case is that I have a config object, defined like this (on the Java backend side):

public record UserManagementConfig(int excessiveFailedLoginLimit) {}

I have defined an RTK query for the config object in my api definition:

        getConfig: builder.query({ query: () => '/config' }),

And I have a mutation for the config object defined like so:

        postConfigModify: builder.mutation({
            query: body => ({ url: '/config', method: 'POST', body }),
            async onQueryStarted(body, { dispatch, queryFulfilled }) {
                try {
                    const { data: configAfterConfigModify } = await queryFulfilled;
                    dispatch(api.util.updateQueryData('getConfig', undefined, () => configAfterConfigModify));
                } catch {}
            },
        }),

So far, so good. Now I would like to edit this object in a form and save it back, but the question is how?

Offhand I could think of the following ways:

  1. Operate directly on the RTK query's cache in redux
  2. Use a redux slice to back the form
  3. Use local state to back the form
  4. Use react-hook-form

Alternatives 2-4 would AFAICT involve useEffect(), and... I try to avoid useEffect() if remotely possible (well, I could have done redux slice without useEffect() but it would have involved calling useGetConfigQuery() without using the results, which feels kinda wasteful.

Long story short: I was able to operate on the RTK query cache of getConfig directly and use that as the post value, but the dispatch() content isn't pretty:

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useGetConfigQuery, usePostConfigModifyMutation, api } from '../api';
import Container from './bootstrap/Container';
import StyledLinkLeft from './bootstrap/StyledLinkLeft';
import FormRow from './bootstrap/FormRow';
import FormLabel from './bootstrap/FormLabel';
import FormField from './bootstrap/FormField';
import ModifyFailedErrorAlert from './ModifyFailedErrorAlert';
import { findSelectedUser } from './common';

export default function Configuration() {
    const { data: config = {} } = useGetConfigQuery();
    const dispatch = useDispatch();
    const [ postConfigModify ] = usePostConfigModifyMutation();
    const onModifyConfigClicked = async () => await postConfigModify(config);

    return (
        <div>
            <nav className="navbar navbar-light bg-light">
                <StyledLinkLeft to="/">Up to user adminstration top</StyledLinkLeft>
                <h1>Configuration</h1>
                <div className="col-sm-2"></div>
            </nav>
            <ModifyFailedErrorAlert/>
            <form onSubmit={ e => { e.preventDefault(); }}>
                <Container>
                    <FormRow>
                        <FormLabel htmlFor="excessiveFailedLoginLimit">Excessive failed login limit</FormLabel>
                        <FormField>
                            <input
                                id="excessiveFailedLoginLimit"
                                className="form-control"
                                type="text"
                                value={config.excessiveFailedLoginLimit}
                                onChange={e => dispatch(api.util.updateQueryData('getConfig', undefined, () => ({ ...config, excessiveFailedLoginLimit: e.target.value })))} />
                        </FormField>
                    </FormRow>
                    <FormRow>
                        <div className="col-5"/>
                        <FormField>
                            <button
                                className="btn btn-primary form-control"
                                onClick={onModifyConfigClicked}>
                                Modify configuration</button>
'                        </FormField>
                    </FormRow>
                </Container>
            </form>
        </div>
    );
}

If I could have written

onChange={e => dispatch(setExcessiveFailedLoginLimit(e.target.value)))} />

it would have taken me a long way towards what I want, I think...?

@EskiMojo14
Copy link
Collaborator

EskiMojo14 commented Feb 10, 2025

I'm confused - are you just asking to do

dispatch(api.util.updateQueryData('getConfig', undefined, (draft) => {
  draft.excessiveFailedLoginLimit = e.target.value;
})

? if so, that's already supported.

@phryneas
Copy link
Member

That said, while this is a form, it doesn't belong into Redux state.
3. and 4. are the right options, and I can't see how you would need an useEffect at any point in time - everything in a form happens as a consequence of user interaction, so anything you do would always be in an event handler?

@steinarb
Copy link
Author

I'm confused - are you just asking to do

dispatch(api.util.updateQueryData('getConfig', undefined, (draft) => {
draft.excessiveFailedLoginLimit = e.target.value;
})
? if so, that's already supported.

It does the job but it isn't very pretty.

I would like something that is prettier.

@markerikson
Copy link
Collaborator

@steinarb all those are pretty much necessary. RTKQ needs to know how to find the specific cache entry you're looking for, and that requires the endpoint name and same query args you passed to the hook. Doing that work requires dispatching an action, same as any other Redux update.

Out of curiosity, what would a "prettier" syntax look like for you?

@steinarb
Copy link
Author

That said, while this is a form, it doesn't belong into Redux state.

Why not? I've been backing forms with redux data since I started using react (and redux).

Works perfecly fine (for me). When I use something else I miss the redux devtool.

  1. and 4. are the right options, and I can't see how you would need an useEffect at any point in time - everything in a form happens as a consequence of user interaction, so anything you do would always be in an event handler?

Without useEffect() how do you transfer the data from useGetConfigQuery() to the local state?

@phryneas
Copy link
Member

Without useEffect() how do you transfer the data from useGetConfigQuery() to the local state?

Either you pass it in from a parent component and use a key for a reset, or you set it in render.

See https://react.dev/reference/react/useState#storing-information-from-previous-renders

const { data } = useGetConfigQuery()
const [previousData, setPreviousData] = useState(data)

const [formState, setFormState] = useState(() => getInitialFormState(data))

if (data !== previousData) {
  setPreviousData(data)
  setFormState(getInitialFormState(data)))
}

Why not? I've been backing forms with redux data since I started using react (and redux).

In the FAQ since 2016 or 2017: https://redux.js.org/faq/organizing-state#should-i-put-form-state-or-other-ui-state-in-my-store

And in the Style Guide since it exists: https://redux.js.org/style-guide/#avoid-putting-form-state-in-redux

@steinarb
Copy link
Author

@steinarb all those are pretty much necessary. RTKQ needs to know how to find the specific cache entry you're looking for, and that requires the endpoint name and same query args you passed to the hook. Doing that work requires dispatching an action, same as any other Redux update.

Yes, I know (I have been messing around with updateQueryData on mutation completion for two months now)

Out of curiosity, what would a "prettier" syntax look like for you?

Instead of

onChange={e => dispatch(api.util.updateQueryData('getConfig', undefined, () => ({ ...config, excessiveFailedLoginLimit: e.target.value })))}

it would be nice to have something like:

onChange={e => dispatch(setExcessiveFailedLoginLimit(e.target.value)))}

(can I do it with a wrapper function I wonder...? The main problem is I need to "see" config, which would have been "state" in a reducer, to be able to set a particular property of the "slice"... hm...)

@EskiMojo14
Copy link
Collaborator

there's no way we could generate that for you, but it would absolutely be possible to create a wrapper for:

export const setExcessiveFailedLoginLimit = (value) => api.util.updateQueryData('getConfig', undefined, (draft) => { draft.excessiveFailedLoginLimit = value })

dispatch(setExcessiveFailedLoginLimit(e.target.value)))

@steinarb
Copy link
Author

Even though I made it work using the redux cache directly, it wasn't pretty.

So I swapped it today to use a redux slice to back the form. steinarb/authservice@b41c188

A few more lines but more readable.

With a few lines more I was able to disable the submit button until the form has a change.
steinarb/authservice@e721a9c

@EskiMojo14
Copy link
Collaborator

we'd still recommend using a proper form library for managing forms, not redux 😄 redux isn't well suited to actions being dispatched for every keystroke, for example

@steinarb
Copy link
Author

Not trying to pick a fight but I have been using redux this way since I started using react (and redux) back in 2018, and I haven't noticed any ill effects... so not sure I understand what the problem is with actions being dispatched for every keystroke is...?

I have tried using local state and found out that that was easy until the time came to replace the form data with fresh data, then not so pretty.

And of course I can examine everything that's going on with redux devtools (which was the major reason I stayed with redux when others left for local state). https://steinar.bang.priv.no/2022/06/28/yep-im-still-using-redux/

@markerikson
Copy link
Collaborator

@steinarb It's primarily about performance. You end up dispatching actions, which run reducers + subscribers + selectors, when only a local part of the UI cares about the results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants