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

Support for collection of nested models #829

Closed
yard opened this issue Apr 20, 2023 · 5 comments
Closed

Support for collection of nested models #829

yard opened this issue Apr 20, 2023 · 5 comments

Comments

@yard
Copy link
Contributor

yard commented Apr 20, 2023

Hi there,

Not so much of an issue but rather a question: is it possible to maintain a dynamic collection of slices within a single model?

A dummy example: I want to have Company store which would contain multiple Department slices. Company store would operate departments on a higher level (add, remove, rename etc), whereas Department slice would hold actions for manipulating a single Department (e.g. manage a collection of users within). The key requirement is to keep the state separate for each department yet have them organized under a single parent store for higher-level manipulations.

I know we can use addModel to add a single slice to achieve a similar goal for a single slice, but it does not seem to be well-suited to support collections of slices (each behaving as an isolated element). An alternative would be to make friends with immer and keep a collection of custom class instances in the Company store, but that obviously breaks actions/thunk/computed property support etc with this custom class.

@jmyrland
Copy link
Collaborator

hey @yard !

A dummy example: I want to have Company store which would contain multiple Department slices. Company store would operate departments on a higher level (add, remove, rename etc), whereas Department slice would hold actions for manipulating a single Department (e.g. manage a collection of users within). The key requirement is to keep the state separate for each department yet have them organized under a single parent store for higher-level manipulations.

I'm guessing in this case, that you would like a dynamic set of companies, each with their respective departments. And that you would prefer an "object oriented" approach, e.g. company.addDepartment(...).

As far as I know, this feature does not currently exist. (Correct me if I'm wrong, @no-stack-dub-sack / @ctrlplusb)

This is how I would solve this particular scenario:

import { action, Action } from "easy-peasy";

interface ICompany {
  id: string;
  name: string;
  departments: IDepartment[];
}

interface IDepartment {
  name: string;
  employees: IEmployee[];
}

interface IEmployee {
  name: string;
}

export interface StoreModel {
  companies: ICompany[];

  addOrUpdateCompany: Action<this, ICompany>;
}

const storeModel: StoreModel = {
  companies: [
    {
      id: "umbrella-corp",
      name: "Umbrella",
      departments: [
        {
          name: "International Investigation Department",
          employees: [{ name: "P.T." }, { name: "O'Neal" }]
        },
        {
          name: "North American division",
          employees: [{ name: "Lauper, Douglas" }]
        }
      ]
    }
  ],

  addOrUpdateCompany: action((state, newComp) => {
    const existingCompIndex = state.companies.findIndex(
      (c) => c.id === newComp.id
    );

    if (existingCompIndex !== -1) {
      state.companies[existingCompIndex] = newComp;
    } else {
      state.companies = [...state.companies, newComp];
    }
  })
};

export default storeModel;

This does not solve your issue, but it exposes the opportunity to create a functional wrapper for each company via a custom hook - utilizing the addOrUpdateCompany action, e.g:

const useCompany = (company: ICompany) => {
  const addOrUpdateCompany = useStoreActions(
    (store) => store.addOrUpdateCompany
  );

  return {
    // Keep state
    ...company,
    // Extend with actions
    addDepartment: (newDepartment: IDepartment) => {
      addOrUpdateCompany({
        ...company,
        departments: [...company.departments, newDepartment]
      });
    },
    // Extend departments state with actions
    departments: company.departments.map((department, dIndex) => ({
      // Keep state
      ...department,
      // Extend with actions
      addEmployee: (newEmployee: IEmployee) =>
        addOrUpdateCompany({
          ...company,
          departments: company.departments.map((d, index) =>
            dIndex === index
              ? {
                  ...department,
                  employees: [...department.employees, newEmployee]
                }
              : d
          )
        })
    }))
  };
};

This is an abstraction on top of a single company - allowing you to add departments, as well as adding employees to a specific department.

const company = useCompany(companyState);

company.addDepartment({...});

company.departments[0].addEmployee({...});

I've created a sandbox to verify this implementation, if you would like to take a deeper look.

@yard
Copy link
Contributor Author

yard commented Apr 20, 2023

Hi @jmyrland!

Thank you for a throughout example. Yep your approach is legit, but it essentially provides shortcuts to otherwise global actions operating on the whole state. Another take would be to avoid hooks and just capture references to store's actions and use them to craft "methods" on the nested entities:

const storeModel = {
  departments: [],

  _addOrUpdateDepartment: action((state, [department, actions]) => {
    const index = state.departments.indexOf(department);

    if (index === -1) {
      state.departments.push(department);
    } else {
      state.departments[index] = department;
    }

    department.save = () => actions._addOrUpdateDepartment(department);
    department.delete = () => actions._removeDepartment(department);
  }),

  _removeDepartment: actions(() => {
    const index = state.departments.indexOf(department);
    state.departments.splice(index, 1);
  }),

  addDepartment: thunk((actions, department) => {
    actions._addOrUpdateDepartment([department, actions]);
  })
};

What I was trying to achieve, however, is to have a separate slice of state per entity (department in my example) and use all the good things like actions, thunks, computed props etc. I do appreciate it might not be exactly possible though.

@jmyrland
Copy link
Collaborator

What I was trying to achieve, however, is to have a separate slice of state per entity (department in my example) and use all the good things like actions, thunks, computed props etc. I do appreciate it might not be exactly possible though.

It might be possible, but I currently do not know a way to achieve this in the current version of easy-peasy.

@no-stack-dub-sack
Copy link
Collaborator

no-stack-dub-sack commented Apr 21, 2023

@yard So-if I understand you correctly, ideally you'd want something like this?

interface DepartmentModel {
  id: string;
  name: string;
  employees: IEmployee[];
  addEmployee: Action<this, IEmployee>;
}

interface StoreModel {
  addDepartment: Thunk<this, IDepartment | IDepartment[]>;
  removeDepartment: Thunk<this, string | string[]>;
  departments: DepartmentModel[]; // entries are dynamically added slices
}

@no-stack-dub-sack
Copy link
Collaborator

@yard Closing this for the time being pending any further questions. Feel free to reopen if you'd like to continue the discussion. Thanks!

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

3 participants