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

Improve createEntityAdapter tutorials #961

Open
aaronadamsCA opened this issue Mar 30, 2021 · 3 comments
Open

Improve createEntityAdapter tutorials #961

aaronadamsCA opened this issue Mar 30, 2021 · 3 comments
Labels

Comments

@aaronadamsCA
Copy link

aaronadamsCA commented Mar 30, 2021

Hello! I'm really enjoying learning React Redux thanks to Redux Toolkit. Compared to my first attempt a couple of years ago, Redux Toolkit makes learning Redux far more approachable. 😀

I see that #371 recently reorganized tutorials; speaking as a first-time learner, the new setup is great. I've been making my way across all three sites, and you've been mindful enough of avoiding overlap that I've rarely found conflicting information.

As I try to learn createEntityAdapter in greater detail, though, I'm running into some gaps:

  • Examples and tutorials all assume entities are used together with thunks. For example, all existing examples of extraReducers use thunk actions.
  • I can't find any coverage of entity relationships. For example, if my sample todos application had many lists, how should I structure my slices? How should I define extraReducers? Are there any special TypeScript considerations?

I think it would be more approachable if I could first learn createEntityAdapter, then entity relationships, and only then how to integrate entity adapters with thunks.

I've searched extensively without finding a good resource like this. I think I've pushed through my own confusion, and I think I should be able to work it out on my own at this point; but after spinning my wheels all morning I wanted to provide the feedback. Thanks again!

@markerikson
Copy link
Collaborator

Hey, thanks for the feedback! Glad to know that the docs restructuring is working out okay.

I'm not quite sure what you're asking about with regards to entity adapters and thunks. Can you clarify? Ultimately, the entity adapter just provides prebuilt reducer logic, and you can run that logic in response to whatever actions you want to define.

Regarding the tutorials, there's three topics I was trying to teach there:

  • Principles of normalizing state in general
  • Working with data that's fetched from a server
  • Using the entity adapter specifically to manage normalized state

Since it's a tutorial, and a project-based one specifically, it can't cover all possible use cases. The goal is to introduce overall principles, introduce the available APIs, and give you a chance to practice working with those APIs. Think of it as the instruction booklet for a Lego set. We show you the basic intended way that the blocks should snap together, but from there you should now have enough info to take that set apart and rearrange it to build something different using the same blocks.

The info you're asking for would really be more suited for a "usage guide" page rather than a "tutorial" page (see https://documentation.divio.com/ for info on the distinction between the different doc formats).

The entity adapter APIs currently don't provide any special capabilities regarding relationships, and to be honest I haven't tried to actually work out any relationship-oriented usage patterns myself. (I've got wayyyy to many other things on my plate :) ) Agreed that it would be useful to have some info on that topic, but I don't have any specific advice to offer there atm.

I'll note that my old "Practical Redux" blog tutorial series does show using https://github.com/redux-orm/redux-orm to manage relational state in Redux. Today the entity adapter does cover a good chunk of those use cases, but if your data is very relational that may still be something worth evaluating.

@aaronadamsCA
Copy link
Author

Thanks for the thoughtful response, @markerikson.

I haven't tried to actually work out any relationship-oriented usage patterns myself

That's probably the part I struggled with the most as a newcomer. To borrow your Lego analogy, it felt like I was shown how to use several pieces together as one, but not how to use any of those pieces on their own. Working that out on my own was a challenge. There are plenty of examples of, say, fetching external relational data and then using Normalizr; but I couldn't find much about building and managing entity relationships within the client.

I ended up using two entity adapters within a single slice, with my reducers manipulating both entities. Here's a complete example:

import type { PayloadAction } from "@reduxjs/toolkit";
import { createEntityAdapter, createSlice } from "@reduxjs/toolkit";

type Cart = {
  id: string;
  lineItemIds: Array<LineItem["id"]>;
};

type LineItem = {
  id: string;
  quantity: number;
  cartId: Cart["id"];
};

type CreatePayload = Pick<LineItem, "id" | "cartId">;
type UpdatePayload = Pick<LineItem, "id" | "quantity">;
type DeletePayload = Pick<LineItem, "id">;
type ClearCartPayload = Pick<Cart, "id">;

const carts = createEntityAdapter<Cart>();
const lineItems = createEntityAdapter<LineItem>();

export const cartsSlice = createSlice({
  name: "carts",
  initialState: {
    carts: carts.getInitialState(),
    lineItems: lineItems.getInitialState(),
  },
  reducers: {
    addLineItem: (state, { payload }: PayloadAction<CreatePayload>) => {
      const { id, cartId } = payload;

      // Initialize the cart if it doesn't already exist
      carts.addOne(state.carts, { id: cartId, lineItemIds: [] });

      const cart = state.carts.entities[cartId];

      if (cart?.lineItemIds.includes(id) === false) {
        cart.lineItemIds.push(id);
      }

      lineItems.addOne(state.lineItems, { id, quantity: 1, cartId });
    },

    updateLineItem: (state, { payload }: PayloadAction<UpdatePayload>) => {
      const { id, quantity } = payload;

      lineItems.updateOne(state.lineItems, {
        id: id,
        changes: { quantity },
      });
    },

    removeLineItem: (state, { payload }: PayloadAction<DeletePayload>) => {
      const { id } = payload;

      const lineItem = state.lineItems.entities[id];

      if (lineItem) {
        const cart = state.carts.entities[lineItem.cartId];

        if (cart) {
          cart.lineItemIds = cart.lineItemIds.filter((value) => value !== id);

          if (cart.lineItemIds.length === 0) {
            carts.removeOne(state.carts, cart.id);
          }
        }
      }

      lineItems.removeOne(state.lineItems, id);
    },

    clearCart: (state, { payload }: PayloadAction<ClearCartPayload>) => {
      const { id } = payload;

      const lineItemIds = state.carts.entities[id]?.lineItemIds;

      if (lineItemIds?.length) {
        lineItems.removeMany(state.lineItems, lineItemIds);
      }

      carts.removeOne(state.carts, id);
    },
  },
});

I have no idea if this is beautiful, hideous, or somewhere in between. Maybe this should have been split into two slices and used extraReducers; maybe there are better ways to maintain the relational arrays; maybe I don't need to store the IDs on both sides of the relationship.

At any rate, I'd love to see more documentation emerge on this subject to help me figure this out further.

@kasper573
Copy link

kasper573 commented Apr 13, 2021

I would also be interested in seeing examples of good practices on how to do normalized relational data with redux toolkit. A recent project of mine does this heavily, and I'm struggling to utilize RTK slices, since most (if not all) delete operations for any relational data would either require the slice to modify state outside its slice (if I do one slice per model), or I'd have to use extraReducers and listen to all related deletes. The latter I tried, but wasn't able to make it work (probably due to my lack of experience with RTK). Examples for these type of scenarios would be really valuable!

My aproach to normalized relational data is very much like @aaronadamsCA:
reducers
state

I pretty much just have a flat list of reducers, no slices, each using createReducer with a default case. Every reducer has access to all state, but generally just mutates a small slice (which is why I'd like to refactor to slices), except for most delete operations that reuse the delete reducers for their related entities. It feels really hacky to reuse reducers in other reducers, so as a minimum I'd like to refactor to use slices with extraReducers to react to related deletes.

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

No branches or pull requests

3 participants