A simple bug when using Redux Toolkit that took me a while to figure out

I have been using React and redux for a few years, and got quite comfortable with building moderately complex component logic with them. I recently migrated an app over to using Redux Toolkit, motivated by wanting to stay up to date with the latest recommendation, as well as reducing boilerplate, which can be cumbersome with redux action creators and reducers.

When using Redux Toolkit, often times you’d need to declare extraReducers to act upon actions defined in a different slice. That would look like so

const asyncAction = createAsyncThunk(
  'async/action',
  async (foo) => {
    await doAsync();
    return 42;
  }
)

const form = createSlice({
  name: 'form'
  initialState,
  reducers: {
    ...
  },
  extraReducers: (builder) => {
    builder.addCase(actionOne, (state, action) => {
    })
    .addMatcher((action) => [
      actionTwo.type,
      actionThree.type,
      asyncAction.fulfilled.type
    ].includes(action.type), (state) => {})
  }
});

You would use addMatcher to match on multiple action types. This does become a bit verbose, so redux-toolkit does provide a helper function called isAnyOf to make this a bit easier

builder.addMatcher(isAnyOf(
  actionTwo,
  actionThree,
  asyncAction.fulfilled
), (state) => {});

This is pretty straightforward. However, in the process of doing the refactoring to use the helper, I made a mistake of forgetting to remove the .type attribute on the action

builder.addMatcher(isAnyOf(
  actionTwo.type,
  actionThree.type,
  asyncAction.fulfilled
), (state) => {});

This might have been simple bug to fix had I known where to look. However, it manifested in a pretty strange behavior, where the matcher would match even when a different action is fired. This caused a pretty confusing behavior in my case, due to an inadvertent state logic:

const initialState = {
  loading: false,
};
const form = createSlice({
  name: 'form',
  initialState,
  reducers: {
    submit: (state) => {
      state.loading = true;
    }
  },
  extraReducers: (builder) => {
    builder.addMatcher(
      isAnyOf(
        submitFailure.type,
        validationFailure.type,
        refresh.fulfilled
    ), (state) => {
      state.loading = false;
    });
  };
});

So I was in a situation where, upon calling the submit action, I can see that the action was fired, but the loading state did not become true. From just reading the code, it would never occur to me that the matcher in the extraReducers was invoked. This was especially tricky to realize, because the actual code contains many more lines of complex logic and a high number of reducers to parse through. It is still unclear to me why the isAnyOf matcher would behave that way. I suspect it has to do with the fact that the helper can accommodate many different types of arguments. I wonder if TypeScript check would have caught this issue here.

comments powered by Disqus