Search code examples
javascriptreactjsreact-reduxredux-toolkitrtk-query

Systematically create multiple instances of custom hook to extend store entities with additional methods


Let's say I have a hook that returns an entity's state, as well as extends it with some methods for enacting mutations:

// Loads an entity from the store and provides methods to update and remove it
const useEntity = (entityId) => {
  // Selects the entity from the store
  const entity = useSelector(selectEntity(entityId));

  // Updates the entity in the store
  const update = (value) => {
    // ...logic
  };

  // Removes the entity from the store
  const remove = () => {
    // ...logic
  };
  
  return {
    entity, // The entity from the store
    update, // The update method
    remove  // The remove method
  };
};

I would like to create a "wrapper" hook that "extends" all entities in the store using my useEntity hook

// Extends EVERY entity in the store with the useEntity hook defined previously
const useEntities = () => {
  // Selects all entities from the store
  const allEntities = useSelector(selectAllEntities);
  const extendedEntities = allEntities.map((entity) => useEntity(entity.id));
  return extendedEntities;
}

Obviously, the logic in useEntities violates the Rules of Hooks as the hooks get called inside a loop.

What then is the correct way to extend my entities to have the additional custom methods?

Ideally, I'd like to be able to do something like

const extendedEntities = useEntities()
  extendedEntities[0].update("New value") // Updates the value for entity 0
  extendedEntieis[6].remove() // Removes entity 6
  extendedEntities[3].entity // returns the value of entity 3
  // ...etc.

Solution

  • Instead of relying so heavily on the useSelector hook you could modify the useEntity hook to be a augmentEntity utility that is either passed or closes over a reference to the store object where it can internally select the state it requires.

    In other words, recall that selectEntity(entityId) is a selector function that is passed the current Redux state and returns a computed value. Convert useSelector(selectEntity(entityId)) to selectEntity(entityId)(state).

    Something like the following:

    /** Loads an entity from the store and provides methods to update and remove it */
    const augmentEntity = (entityId) => {
      const state = store.getState();
    
      // Selects the entity from the store
      const entity = selectEntity(entityId)(state);
    
      // Updates the entity in the store
      const update = (value) => {
        // ...logic
      };
    
      // Removes the entity from the store
      const remove = () => {
        // ...logic
      };
      
      return {
        entity, // The entity from the store
        update, // The update method
        remove  // The remove method
      };
    };
    
    const useEntities = () => {
      // Selects all entities from the store
      const allEntities = useSelector(selectAllEntities);
    
      // Augments all entities with update/remove functions
      return allEntities.map((entity) => augmentEntity(entity.id));
    };
    

    Since selectAllEntities already returns all the entities there is probably no reason to again select specific entities directly, just pass the mapped entities instead of their ids to the utility and augment as needed.

    Example:

    const augmentEntity = (entity) => {
      // Updates the entity in the store
      const update = (value) => {
        // ...logic
      };
    
      // Removes the entity from the store
      const remove = () => {
        // ...logic
      };
      
      return {
        entity, // The entity from the store
        update, // The update method
        remove  // The remove method
      };
    };
    
    const useEntities = () => {
      // Selects all entities from the store
      const allEntities = useSelector(selectAllEntities);
    
      // Augments all entities with update/remove functions
      return allEntities.map(augmentEntity);
    };