lostpebble / pullstate

Simple state stores using immer and React hooks - re-use parts of your state by pulling it anywhere you like!

Home Page:https://lostpebble.github.io/pullstate

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Question: Update element within nested array

hohenp opened this issue · comments

Hello

thanks for that great module. I am in the moment replacing redux with pullstate in our solutions and facing a problem, that not might be a pullstate thing, but I am wondering if there is a specific way to do it using pullstate and update.

In my Store I am having an Array of so called "PlanningElements" which contain PlanningElements as childrens again. So I get like a tree structure. Now I have to update a property of a specific PlanningElement anywhere in the tree and I only have the ID of that planninglement.

Thanks very much for your help!!
Pascal


// MY Model for the Tree Structure
export default interface PlanningElement {    
    planningId?:number; // ID of the planned Element
    title?:string;
    children?: PlanningElement[];
}

// MY STORE
interface IPlanningViewStore {    
    meetingsPlanningElements: [{ meetingId: number; planningElements: PlanningElement[] }?];
}

export const PlanningViewStore = new Store<IPlanningViewStore>({    
    meetingsPlanningElements: [],
});



// MY Method, trying to update the element anywhere within the tree structure
// The current implementation only supports elements, at the root level...
// How can I update nested objects
const updatePlanningElementWithinTree = (meetingId: number, updatedPlanningElement: PlanningElement) => {
    PlanningViewStore.update((s) => {
         const elements = s.meetingsPlanningElements.find((m) => m.meetingId == meetingId).planningElements;
         const index = elements.findIndex(e => e.planningId == updatedPlanningElement.planningId);
         s.meetingsPlanningElements.find((m) => m.meetingId == meetingId).planningElements[index] = updatedPlanningElement;
    });
};


Here probably a way as I only have 2 levels within the tree, but maybe you have a more elegant idea?!


 const replacePlanningElementById = (planningElements: PlanningElement[], updatedPlanningElement: PlanningElement) => {
      const nodeIndex = findIndex(planningElements, (t) => t.planningId == updatedPlanningElement.planningId);
      if (nodeIndex > -1) {
           planningElements[nodeIndex] = updatedPlanningElement;
      }

      planningElements.forEach((pe) => {
           const nodeIndex = findIndex(pe.children, (t) => t.planningId == updatedPlanningElement.planningId);
           if (nodeIndex > -1) {
                pe.children[nodeIndex] = updatedPlanningElement;
           }
      });
      return planningElements;
 };

 const updatePlanningElementWithinTree = (meetingId: number, updatedPlanningElement: PlanningElement) => {
      PlanningViewStore.update((s) => {
           const elements = s.meetingsPlanningElements.find((m) => m.meetingId == meetingId).planningElements;
           const updatedElements = replacePlanningElementById(elements, updatedPlanningElement);
           s.meetingsPlanningElements.find((m) => m.meetingId == meetingId).planningElements = updatedElements;
      });
 };

Hi @hohenp ,

Firstly, I just want to point out that you should usually use the second argument to update() when mapping over or searching on the state within your Store during an update. This second argument is the same as the first- but its not using a proxy (the special object which allows us to directly mutate our state and get the changes), which will make it more performant to read and inspect. Not a big deal for small things, but over large collections you might notice something.

Could you maybe do something like this?:

const updatePlanningElementWithinTree = (meetingId: number, updatedPlanningElement: PlanningElement) => {
    // Notice I use "o" here, stands for the "original" un-proxied state
    PlanningViewStore.update((s, o) => {
         const meetingIndex = o.meetingsPlanningElements.findIndex(m => m.meetingId === meetingId);
         // might want to run validation that meetingIndex >= 0
         const planIndex = o.meetingsPlanningElements[meetingIndex].planningElements.findIndex(e => e.planningId === updatedPlanningElement.planningId);
         // might want to run validation that planIndex >= 0
         s.meetingsPlanningElements[meetingIndex].planningElements[planIndex] = updatedPlanningElement;
    });
};

Its usually best when updating a big tree like this, to try and get all the keys you need for the deep update (the indexes here) - and then apply the update exactly to those keys on the state (s.meetingsPlanningElements[meetingIndex].planningElements[planIndex] = updatedPlanningElement;).

Let me know if that helps, or if I missed something!

Hi @lostpebble

thanks for your help and your suggestion with the second parameter within update(). Were not aware of that, thanks!
As my PlanningElement does not have to be on Level 1 and also could be on level 2 I wrote the "replacePlanningElementById" that goes over every element.

So the combination of my initial approach and your improvement could look like:
The downside is, that I always update the root "planningElements" object and I thought there might by a library like immer or deepdash that is able to replace an object within a tree in a more easy way...

const replacePlanningElementById = (planningElements: PlanningElement[], updatedPlanningElement: PlanningElement) => {
      const nodeIndex = findIndex(planningElements, (t) => t.planningId == updatedPlanningElement.planningId);
      if (nodeIndex > -1) {
           planningElements[nodeIndex] = updatedPlanningElement;
      }

      planningElements.forEach((pe) => {
           const nodeIndex = findIndex(pe.children, (t) => t.planningId == updatedPlanningElement.planningId);
           if (nodeIndex > -1) {
                pe.children[nodeIndex] = updatedPlanningElement;
           }
      });
      return planningElements;
 };

const updatePlanningElementWithinTree = (meetingId: number, updatedPlanningElement: PlanningElement) => {    
    PlanningViewStore.update((s, o) => {
         const meetingIndex = o.meetingsPlanningElements.findIndex(m => m.meetingId === meetingId);
         const elements = o.meetingsPlanningElements[meetingIndex].planningElements;  
         const updatedElements = replacePlanningElementById(elements, updatedPlanningElement);               
         s.meetingsPlanningElements[meetingIndex].planningElements = updatedElements
    });
};

Yea, it is somewhat of a challenge to do deep updates.

Look at the bottom section of the immer docs for updates (https://immerjs.github.io/immer/docs/update-patterns), they give the same advice I gave earlier: find the ID of the deep updates you need to do, and then apply them. This is somewhat harder given the structure you are using.

Another option would be to do the updates outside of Pullstate itself, and then just calling Store.replace(newState) with the entire new state tree that you've edited.

Cool, thanks alot @lostpebble . Happy to here, that it is a challange, so braining about that for days was it worth.. :)
Im am closing that and of course we can move the questions to the discussion section. My fault, sorry!!