How should I write this reduct code DRY

These are my reducers:

export default combineReducers({
  courses: coursesReducer,
  books: booksReducer,
  ui: uiReducer
});

I have already coded everything related to the coursesReducer.

In Next.js I have pages/course/[cid].js and I ha CRUD operations as reducer actions and everything. An example, store/actions/courseActions.js:

export const fetchACourse = (payload) => async (dispatch) => {
    const res = await fetch(`http://localhost:1338/courses/${payload.id}`);
    const data = await res.json();
    dispatch({
        type: types.FETCH_A_COURSE,
        payload: data,
    });
}

My dilemma is what everything in booksReducer is identical apart from the fetch URLs: The data structure is exactly the same.

Even though it would involve a lot of refactoring and if statement, do I refactor the code somehow like

export const fetchACourse = (payload) => async (dispatch) => {
    const uri = (payload.type) === 'books' ? `books`: `courses`
    const dispatchType = (payload.type) === 'books' ?  types.FETCH_A_BOOKS :  types.FETCH_A_COURSE
    const res = await fetch(`http://localhost:1338/${uri}/${payload.id}`);
    const data = await res.json();
    dispatch({
        type: dispatchType,
        payload: data,
    });
}

Or should I just create store/actions/courseActions.js and practically duplicate everything and add the minor tweaks?

My opinion is that it’s okay to have up to three sets of duplication.

It can be handy to have those three sets of duplication live and in action, because then you can look for commonalities across them, extracting common functions that can be used across them all, and package that into a single courseReducer that they all access to achieve their needs.

Not sure if I am helping here, but could that be refactored to something like this

// destructure 'type' and 'id' from payload
const fetchACourse = ({type, id}) => 
    async (dispatch) => {
        const res = await fetch(`http://localhost:1338/${type}/${id}`);

        dispatch({
            type: types[`FETCH_A_${type.toUpperCase()}`],
            payload: await res.json()
        });
    }

I know ‘uri’ might be a clearer label, but isn’t it always going to equal payload.type?

You can use bracket notation to dynamically access an object property e.g.

const person = {
    firstName: 'Fred',
    lastName: 'Dibnah'
}

const prop = 'lastName'
console.log(person[prop]) // Dibnah

edit: My solution may well fail with ‘course’ and ‘courses’

That’s true.

That’s the kind of thing I was thinking about. The question is would/should you do that or go for duplication and a bit more clarity instead?

Not an easy one is it?

I don’t know, but leaning more towards clarity I guess. (See Paul is replying :grinning:)

Duplication can be a help when it comes to clarity. The big downside of duplication is when it comes to future ongoing maintenance. A change that happens to one part of the duplication tend to always be needed in the other parts too. Sometimes that needed change doesn’t happen one of the duplicate parts, which is easy to miss too. That then leads to confusion later on about if the nearly similar but not quite part really needs to be different, or if that will break something. In truth it needs to be brought up to date but fear of breaking something results in the code rotting and going bad.

You can tell I’ve been there before.

At minimum I would extract the res and data variables out to functions that can be used by each reducer. That way the each reducer is able to give a broad and clearly understood summary of what gets done, letting you explore the functions if you need to know a lower abstraction about how it gets done.

There is an idea about keeping each section at the same single level of abstraction. Some good details about that are found at Why keeping levels of abstraction matters

What that means is that fetching a course is about using the dispatchType and data to fetch the course. In practice what that means is to extract separate functions from the code until you can’t reasonably extract any more from it. If you were able to extract another useful function then that code is at a different level of abstraction. That way the functions help to keep the code within them at a similar of extraction.

2 Likes

On the topic of fetching entities have you looked into any libraries that can manage all the boilerplate for you? In Angular there is NgRx data which manages all aspects of api interaction across all entities. I’m not sure if something like that exists in react. However, in Angular NgRx all you need to do is declare entities and services will be made available to persist and manage the entities without needing to write or repeat boilerplate reactive code.

Example config:

//import { EntityMetadataMap } from '@ngrx/data';
import { CrudEntityMetadataMap, CrudEntityQueryMapping } from 'crud';

export const entityMetadata: CrudEntityMetadataMap = {
  PanelPageListItem: {
    entityName: 'PanelPageListItem',
    crud: {
      rest: {
        // ops: ['query'],
        params: {
          entityName: 'PanelPageListItem'
        }
      },
      /*idb_keyval: {
        params: {
          prefix: 'panelpage__'
        },
        queryMappings: new Map<string, CrudEntityQueryMapping>([
          ['path', { defaultOperator: 'startsWith' }]
        ])
      }*/
    }
  },
  PanelPage: {
    entityName: 'PanelPage',
    crud: {
      aws_s3_entity: {
        // ops: ['query'],
        params: {
          bucket: 'classifieds-ui-dev',
          prefix: 'panelpages/'
        }
      },
      rest: {
        // ops: ['query'],
        params: {
          entityName: 'PanelPage'
        }
      },
      /*idb_keyval: { // demo only
        params: {
          prefix: 'panelpage__'
        }
      }*/
    }
  },
  PanelPageState: {
    entityName: 'PanelPageState'
  }
};

Each key in the metadata config is a separate entity automatically managed by NgRx data. Dispatching an entity action to save ANY action is as easy as calling one single method and subscribing to the response.

    const panelPageForm = new PanelPageForm({ ...this.pageForm.value, id: uuid.v4() });
    console.log(panelPageForm);
    console.log(this.formService.serializeForm(panelPageForm));
    this.panelPageFormService.add(panelPageForm).subscribe(() => {
      alert('panel page form added!');
    });

The add method on the entity service for every entity is available along with several others to automatically manage persistence and find operations. This is nice because it significantly reduces the amount of boilerplate code needed since the NgRx data packages dynamically creates services to manage every entity defined in the metadata config.

I wonder if something like that exists for react that can be used to reduce the boilerplate reactive javascript for managing your entities.

I for one hate writing all that boilerplate code and have completely been able to avoid it using NgRx data while still using redux.

This is the Angular package although I’m not sure how useful that would be.

https://ngrx.io/guide/data

This looks interesting for react.

3 Likes

I’m totally new at this, in that this is my biggest project ever. I’ll have to re-read what you wrote a few times before I fully understand it.

I’m going to start by duplicating and then refactoring. I’m guessing at basics level it will be something like this:

<button onClick={getBooksReducerAction()}>get books</books>
<button onClick={getCoursesReducerAction()}>get books</books>
export const getBooksReducerAction = (uri) => {
    const res = await fetch('http://books-url');
    const data = await res.json();
    sortData('books', data);
}
export const getCoursesReducerAction = (uri) =>  {
    const res = await fetch('http://courses-url');
    const data = await res.json();
    sortData('courses', data);
}

export const sortData = (type, data) => async (dispatch) => {
   // get data ready here
   if(type === 'books'){
      // or get data ready here depending...
      dispatch({
        type: types.FETCH_BOOKS,
        payload: data,
    });
   }
   if(type === 'courses') {
      // or get data ready here depending...
      dispatch({
        type: types.FETCH_COURSES,
        payload: data,
    });
   }
}

(I realize this might not add anything to the discussion but I wrote it now)

Assuming the idea above is in the right track would you have sort of the following file structure?

store/
    actions
        coursesActions 
           index.js // would have getCoursesReducerAction
        booksActions 
           index.js // would have getBooksReducerAction
/*Would you add a shared folder? */
        sharedActions 
           index.js // would have sortData

Thank you for the vote of confidence, but my area isn’t Angular or Redux. My advice amounts to repeating the work that has already been done by others. I would seriously go with the advice from @windbeneathmywings about this.

1 Like

That looks great. Thanks you