Closed yard closed 1 year ago
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.
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.
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.
@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
}
@yard Closing this for the time being pending any further questions. Feel free to reopen if you'd like to continue the discussion. Thanks!
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 multipleDepartment
slices.Company
store would operate departments on a higher level (add, remove, rename etc), whereasDepartment
slice would hold actions for manipulating a singleDepartment
(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 theCompany
store, but that obviously breaks actions/thunk/computed property support etc with this custom class.