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

HMS-2861: Use RTK for state management #1477

Closed
wants to merge 4 commits into from

Conversation

lucasgarfield
Copy link
Collaborator

This is currently a WIP, there are a few bugs and some clean-up/refactoring is necessary

So far we have considered (thanks to the work of @lavocatt) two possible options for managing the wizard state: storing state in the highest level component via useState() hooks, and storing state via a useContext() hook.

However, there is a 3rd option - we can use Redux (with RTK).

One big advantage is we can take advantage of the Redux dev tools and use them to inspect the state of the wizard during development:
image

Copy link

codecov bot commented Dec 4, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (b3fade4) 77.69% compared to head (338c3b3) 77.52%.
Report is 4 commits behind head on main.

❗ Current head 338c3b3 differs from pull request most recent head 0224a31. Consider uploading reports for the commit 0224a31 to get more accurate results

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1477      +/-   ##
==========================================
- Coverage   77.69%   77.52%   -0.17%     
==========================================
  Files          69       69              
  Lines        2035     2016      -19     
  Branches      560      557       -3     
==========================================
- Hits         1581     1563      -18     
+ Misses        413      412       -1     
  Partials       41       41              

see 5 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b3fade4...0224a31. Read the comment docs.

@lavocatt
Copy link
Contributor

lavocatt commented Dec 6, 2023

Thanks for working on this other way of doing things. I'm still reviewing you PR and will hopefully soon be able to write you my thoughts about it.

@lavocatt
Copy link
Contributor

lavocatt commented Dec 6, 2023

I'm in the middle of the review now and I've noticed a bug. The wizard keeps it's state across multiple spawns. To reproduce the issue, open the wizard, fill up a couple of fields, click on cancel and open it up again. The fields that have been selected in the previous wizard session are still active. The expected behavior is that on every new click on create image the wizard starts with its fields set to the default values.

Copy link
Contributor

@lavocatt lavocatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @lucasgarfield this is a very interesting approach and I'm glad you did the experimentation. The Redux toolkit is quite interesting, and the fact that we can explore the global state at any point in time adds value to this contribution.

In fairness to the other two approaches (the one with the Context and the one with the states lifted up to the wizard's root) let's not forget that the React dev toolkit already allows us to explore the current wizard state:

For the context:

image

For the prop lifting:

image

One advantage as you mentioned on slack for the Redux toolkit is that it allows you to get back in time, one inconvenient is that you always have to scroll a lot to get to the latest version of the global state and it gets mixed up with the states of the RTK queries.

There still work for this PR to be a drop in replacement but you're getting there 🚀

{release.match('centos-*') && <CentOSAcknowledgement />}
{isFetching && (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the fetching spinner and error handling moved to? I can't find them

name: 'wizard',
initialState,
reducers: {
changeRelease: (state, action) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For each update a new function must be written right ? Can you try to come up with a generic one? Like we could have an extra information in the payload about which states gets to be updated and then to find it in the state on change time. That would definitely be a better sell to me.

These functions are just setters right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're reducers. They take a state and an action, and they return a state. In normal Redux state updates should be done immutably, but RTK uses the Immer library which allows us to write mutable state updates.

https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers#writing-reducers

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the link to the doc. I meant that these reducers are equivalent to setters, each changeXyz function affects the action.payload value to a place in the state.

changeEnvironment: (state, action) => {
state.targetStep.targetEnvironment = action.payload;
},
initializeWizard: () => initialState,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initialize could take a payload too. That way we could pass in a previous image request to initialize the state with.

import { RHEL_9, X86_64 } from '../constants';

type wizardState = {
targetStep: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm understanding that your intention is to cluster data based on which step is supposed to update it. I think this kind of isolation from one step to another is essential for maintainability. We never want to have the XStep changing values of the YStep, that would lead to some buggy nightmares down the road.

My main concern here is that there is no actual software protection for that to happen, if in the future someone decides to call changeEnvironment within the ReviewStep they'll have very little friction to do so (just import changeEnvironment and call it later in the component`. This issue is already arising in the former DDFWizard we are refactoring (see https://github.com/RedHatInsights/image-builder-frontend/blob/3e07472826749bc665157178759741ea2c841de9/src/Components/CreateImageWizard/formComponents/ReviewStep.js#L31-L51 in that code chunk the review step is doing some changes to the wizard step, which is less than ideal for the state machine of the wizazrd. )

Outside of the naming convention that gives us a framework to apply the law in the future, can you think of any way of improving this to provide better security in that regard?


import type { RootState, AppDispatch } from './index';

// Use throughout your app instead of plain `useDispatch` and `useSelector`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment, can you elaborate please?

// By default the component doesn't show the Centos releases and only the RHEL
// ones. The user has the option to click on a button to make them appear.
const release = useAppSelector((state) => state.wizard.targetStep.release);
const dispatch = useAppDispatch();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should dispatch be called appDispatch to avoid any confusion with the React dispatch?

const prefetchSources = provisioningApi.usePrefetch('getSourceList');
const { isBeta } = useGetEnvironment();
// TODO: Handle isFetching state (use skeletons)
// TODO: Handle isError state (very unlikely...)
Copy link
Contributor

@lavocatt lavocatt Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking forward to these TODOs to be implemented.

};
};

const initialState: wizardState = {
Copy link
Contributor

@lavocatt lavocatt Dec 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bug with the react state always being the same after closing and reopening is likely to come from here. See the initialState is a const, and we're updating that same object everytime we are calling the changeXyz functions. To avoid that issue you can either not have the initialState as a global const, or you could deep copy the object on re-initialization.


type wizardState = {
targetStep: {
release: 'rhel-93';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to correctly type the release and distributions as they were previously?

targetStep: {
release: 'rhel-93';
architecture: 'x86_64';
targetEnvironment: string[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The targetEnvironment type is less safe than the predecessor it intends to replace.

@lucasgarfield
Copy link
Collaborator Author

Closing this for a fresh start.

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

Successfully merging this pull request may close these issues.

2 participants