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

feat: global redux actions #2301

Merged
merged 9 commits into from
Oct 25, 2024
Merged

feat: global redux actions #2301

merged 9 commits into from
Oct 25, 2024

Conversation

shrouxm
Copy link
Member

@shrouxm shrouxm commented Oct 11, 2024

Description

Please review one commit at a time. git really mangled the diff, so i rewrote the commit history and it's way cleaner/more comprehensible if you go one at a time and read the descriptions.

Ready for first round of review/discussion. Explanation (will crystallize into docs in the code once consensus is reached):

The only functional difference this PR introduces is that there are no longer actions which modify the state over the course of several dispatches. In order to make that functional change, I had to do a decent chunk of refactoring. I only want to merge this PR if there's consensus that the new shape of our redux code is an improvement on the old shape.

Changes to codebase

  • creates a helper function createGlobalReducer which allows you to create a reducer which can act on the entire redux state at once, rather than just a slice at a time
  • rewrites many redux actions which existed for the sole purpose of being called by other redux actions (i.e. were not meant as an interface to UI code) as state mutations, examples:
+export const setSoilData = (
+  state: Draft<SoilState>,
+  soilData: Record<string, SoilData>,
+) => {
+  state.soilData = soilData;
+  state.matches = {};
+};
+
 const soilIdSlice = createSlice({
   name: 'soilId',
   initialState,
   reducers: {
-    setSoilData: (state, action: PayloadAction<Record<string, SoilData>>) => {
-      state.soilData = action.payload;
-      state.matches = {};
-    },
+export const addSiteToProject = (
+  state: Draft<State>,
+  args: {siteId: string; projectId: string},
+) => {
+  state.projects[args.projectId].sites[args.siteId] = true;
+};
 const projectSlice = createSlice({
     ...
-    builder.addCase(
-      addSiteToProject,
-      (state, {payload: {siteId, projectId}}) => {
-        state.projects[projectId].sites[siteId] = true;
-      },
-    );
     ...
 });
-export const addSiteToProject = createAction<SiteKey>(
-  'project/addSiteToProject',
-);
  • rewrites UI-facing redux actions which previously dispatched actions to different slices to instead be handled by global reducers which use the new state mutations. example:
const soilIdSlice = createSlice({
     ...
-    builder.addCase(fetchSoilDataForUser.pending, state => {
-      state.status = 'loading';
-    });
-
-    builder.addCase(fetchSoilDataForUser.rejected, state => {
-      state.status = 'error';
-    });
-
-    builder.addCase(fetchSoilDataForUser.fulfilled, state => {
-      state.status = 'ready';
-    });
     ...
});
...
-export const fetchSoilDataForUser = createAsyncThunk(
-  'soilId/fetchSoilDataForUser',
-  dispatchByKeys(soilDataService.fetchSoilDataForUser, () => ({
-    projects: setProjects,
-    sites: setSites,
-    projectSoilSettings: setProjectSettings,
-    soilData: setSoilData,
-    users: setUsers,
-  })),
-);

+export const fetchSoilDataForUser = createAsyncThunk(
+  'soilId/fetchSoilDataForUser',
+  soilDataService.fetchSoilDataForUser,
+);
+
+export const soilIdGlobalReducer = createGlobalReducer(builder => {
+  builder.addCase(fetchSoilDataForUser.fulfilled, (state, {payload}) => {
+    setProjects(state.project, payload.projects);
+    setSites(state.site, payload.sites);
+    setUsers(state.account, payload.users);
+    setProjectSettings(state.soilId, payload.projectSoilSettings);
+    setSoilData(state.soilId, payload.soilData);
+    updateSoilIdStatus(state.soilId, 'ready');
+  });
+
+  builder.addCase(fetchSoilDataForUser.pending, state => {
+    updateSoilIdStatus(state.soilId, 'loading');
+  });
+
+  builder.addCase(fetchSoilDataForUser.rejected, state => {
+    updateSoilIdStatus(state.soilId, 'error');
+  });
+});

features of the new shape of the code

  • IMO this looks closer to how the code would look if we had a DB, which is a pro in my mind. i wanted to stop working before the PR got too big, but it feels like one possible extension of this refactor is to have a separation between a slice's internal code, responsible for monkeying around with the state object, and a set of slice-neutral external-facing actions which call the slices' mutations. as opposed to the previous situation where we do the monkeying and API defining and such all in one big glob
  • there's always been friction in the codebase because of the way dispatched actions are not composable/reusable (you can't dispatch within code resolving an action). the mutations are more composable
  • this makes heavier (or more explicit/visible use) of redux toolkit's transitive dependency immer, which allows you to record mutations against an immutable object without actually mutating the underlying object. this is another thing that makes it seem more DB like, an immer instance around the entire app state looks a lot like a transaction 🤔
  • from what i have seen, this is the less idiomatic redux pattern
    • more idiomatic would be to have code in each slice affected by an action handling that action, but this has such poor locality of behavior that i wanted to try this alternative use (which is still mentioned as an option by the official docs)
    • carissa also pointed out that this would not even be possible without moving the account slice into the mobile client, which i'm not opposed to doing, but feels like more evidence that this approach creates spaghetti interdependencies that we would like to avoid

Checklist

  • Corresponding issue has been opened
  • organize the code a better
  • post associated client-shared PR
  • write documentation and PR annotations to explain changes
  • create some tests

Related Issues

Fixes #2162

Verification steps

Everything should work the same.

@shrouxm shrouxm force-pushed the feat/global-redux-actions branch 2 times, most recently from c0d314f to 232b743 Compare October 14, 2024 20:42
@shrouxm shrouxm force-pushed the feat/global-redux-actions branch from 232b743 to 502375e Compare October 14, 2024 21:01
@shrouxm shrouxm force-pushed the feat/global-redux-actions branch from 502375e to 7d4c0c9 Compare October 14, 2024 21:38
@shrouxm shrouxm force-pushed the feat/global-redux-actions branch from 7d4c0c9 to 1b67911 Compare October 14, 2024 22:40
@shrouxm shrouxm marked this pull request as ready for review October 14, 2024 23:44
@shrouxm shrouxm merged commit 6421ae1 into main Oct 25, 2024
10 checks passed
@shrouxm shrouxm deleted the feat/global-redux-actions branch October 25, 2024 00:15
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

Successfully merging this pull request may close these issues.

refactor: resolve async redux actions in a way that maintains state consistency
2 participants