DEV Community

loading...

Faster development of a maintainable React application

Mahdi Pourismaiel
・4 min read

Developing applications can be a real hassle. It's almost always a dilemma between faster development, maintainability, performance and in case of front end web development, size. It's not easy to get everything right, and of course, there never is just one right way and sometimes, there is none.

I've been thinking about this since I've come back to freelancing. It's important that the project is delivered on time (and even better, sooner than the deadline) and both the code and the result to be in good shape. But sometimes there are roadblocks that keep you from writing the solutions you're used to.

For example, it's much easier if GraphQL is used for the API or even REST is followed but since the backend was developed in a hurry you don't have the luxury of a predictable API. Sometimes there is no design language, there are weird flows or a lot of other factors that mean you have to put in extra time to make up for them.

Projects like Create React App make it easier to start a project. You can use React Router, Redux, Redux Saga to manage the state and routing of the project, use Bootstrap or some other UI library for the UI development but after that, there isn't really much else.

To fix these issues, I think it's really good if you take some time and write some code in the beginning of the project that would remove some of the further code duplication.

For example, dealing with API is something that I usually am able to deal with using a (horrifying) Saga.

import { Action } from 'redux';

export interface IAction extends Action {
  payload?: {
    base_type?: any;
    data?: any;
    error?: any;
    url?: any;
    callback?: {
      onSuccess?: any;
      onFailure?: any;
    };
    [extraProps: string]: any;
  };
  meta?: any;
}

// Action type presets. These are used to determine if an action is created to make an API call (or the state of the API call) or not.
export const baseType = (type: string): string => base + type;
export const stripBase = (type: string): string => type.indexOf(base) === 0 ? type.replace(`${base}/`, '') : type;

export const requestType = (type: string): string => baseType(type + '+REQUEST');
export const successType = (type: string): string => baseType(type + '+SUCCESS');
export const failureType = (type: string): string => baseType(type + '+FAILURE');

export const postType = (type: string): string => baseType(post + type);
export const putType = (type: string): string => baseType(put + type);
export const getType = (type: string): string => baseType(get + type);
export const deleteType = (type: string): string => baseType(deleteT + type);

function makeRequestWorker(method: 'get' | 'post' | 'put' | 'delete') {
  return function*(action: IAction) {
    const type = stripBase(action.type);
    if (!action.payload) {
      throw Error('payload is undefined');
    }

    try {
      // Create a REQUEST action. Can be used to create loading states.
      yield put({ type: requestType(type), meta: action.meta });

      // Fetch data to the server.
      const { data } = yield call(
        axios[method],
        action.payload.url,
        action.payload.data
      );
      yield put({ type: successType(type), payload: data, meta: action.meta });
      if (action.payload.callback && action.payload.callback.onSuccess) {
        yield call(action.payload.callback.onSuccess);
      }
    } catch (err) {
      yield put({
        type: failureType(type),
        payload: { error: err.message },
        meta: action.meta
      });
      if (action.payload.callback && action.payload.callback.onFailure) {
        yield call(action.payload.callback.onFailure);
      }
    }
  };
}

const matchPattern = (pattern: string) => {
  const regex = new RegExp(pattern);
  return (action: IAction) =>
    regex.test(action.type) && action.type.indexOf('+') === -1;
};

// Following functions look for rest actions and if they find any, they'll run
// the appropriate worker.

export function* watchPosts() {
  yield takeEvery(matchPattern(postType('.*')), makeRequestWorker('post'));
}

export function* watchPuts() {
  yield takeEvery(matchPattern(putType('.*')), makeRequestWorker('put'));
}

export function* watchGets() {
  yield takeEvery(matchPattern(getType('.*')), makeRequestWorker('get'));
}

export function* watchDeletes() {
  yield takeEvery(matchPattern(deleteType('.*')), makeRequestWorker('delete'));
}

export default function* sagas() {
  yield* [
    fork(watchPosts),
    fork(watchPuts),
    fork(watchGets),
    fork(watchDeletes)
  ];
}

This badly written code check the incoming action for the API call method, makes the request with that method and the payload.data to payload.url. It also creates request, success and failure actions.

The code may be confusing, but the result is that I can create actions and get the data I want from the reducer without creating any other action creators.

There are a lot of situations like this where the code you're writing feels duplicated. I get this feeling when writing reducers as well. Reducers are fantastic functions to deal with incoming data and manage them to create the new state (or part of the state) of the application. I actually created a small library just to make writing Redux related code a bit faster.

Doing this boilerplating kind of thing in the beginning of each project has made my life quite easier. The time saved in doing so can be used in order to find performance issues, fixing bugs, adding more features and making the application better.

It's not always easy to make use of other people's libraries even if they are very well documented and have great communities. If you think some abstraction you made can be used in other projects, you can always create a package out of it.

These are just examples and I don't think it would be much use to anyone else's development flow. But all suggestions are welcome.

Let me know if you have any "boilerplate" code that has made life a bit easier.

Discussion (0)