decoupling-logic-cover.png
Luciano Ratamero

Luciano Ratamero

23 Jul 2019 5 min read

Decoupling logic from react components

Whenever there’s a new React project, most frontend developers will fumble around with the basic configurations. Patterns of style implementation, component decoupling and folder structure will emerge - not always for the good. The worst part is that every single frontend dev I’ve ever seen will solve the biggest problem of them all, the business logic conundrum, in a different way. In an effort to create a standard to solve the domain layer issue at Labcodes, I’ve researched a bit and found a good and sustainable way to deal with requests and data processing. The end result: react-redux-api-tools.

Let’s imagine a common scenario: CRUD

Since javascript is too permissive, there are endless ways to make a CRUD SPA. Limiting the scope to react helps a bit, but there are still way too many different possible implementations. One of them is to use lifecycle hooks to make requests. Your code may look a bit like this:

export default class Product extends React.Component {
  // ...
  componentDidMount() {
    fetch("/api/products/1/")
      .then(response => response.json())
      .then(data => this.setState({...data}));
  }
  //...
}

I think I don’t need to say this, but I’ll do it anyway: this implementation, even though it’s completely valid, has some big drawbacks.

1.The request is being made inside a component, which, in theory, should have only one job: render data. If, for example, you needed to clear out a user’s session or any other business logic, it would probably be here, and that’s even worse;
2. The response data lives inside the component’s state, and that means that it’s gone as soon as the component unmounts;
3. You’ll have to always fetch the data for each instance of the component, even if that specific request was already done a billion times;
4. There is no specific middle state between the request start and the response, so no loading spinners (though you may be able to implement it using setState callback hells);
5. Since react components are hierarchical, if you need this data inside a child or a parent component, you’ll need to implement contexts/props and callbacks. It gets messy. FAST.

To make it better, most of us prefer to use a library to provide a global application state; one that lives outside all components.

Second step: use Redux

Redux is one of the most amazing tools for the job. So let’s say you’re using redux to manage the data and business logic. Assuming you’ll be using the reducers to deal with business logic and data formatting, your component code will probably look a bit like this:

export default class Product extends React.Component {
  // ...
  componentDidMount() {
    const { fetchProductIsLoading, setProductData } = this.props;
    // turns on the loading spinner via redux
    fetchProductIsLoading();

    // starts the request and puts data on the redux's store
    fetch("/api/products/1/")
      .then(response => response.json())
      .then(data => setProductData(data));
  }
  //...
  render() {
    const { productIsLoading, productData } = this.props;
    // ...
  }
}

This solves most of the issues, but I would argue that this solution makes everything even more coupled and, for sure, worse to read and maintain. The root cause of the issue is that the component is still being responsible for everything: fetching the data and orchestrating redux actions, when it’s only supposed to... render stuff. The ideal flow to solve all issues, at least the best for most cases, would be something akin to the following flowchart:

  • Component mounts and dispatches isLoading signal to redux
  • Redux's action starts the request and triggers isLoading reducer
  • Component renders loading spinner
  • Request is fullfilled asynchronously and triggers success or failure reducer
  • Component renders sucess or failure based on redux's data

Note that the only way to remove completely the business logic from the component is to trigger the ‘Success’ and ‘Failure’ use cases outside of the component. Components should render stuff, not deal with application-wide state management. That means that the component should only dispatch one event (the ‘get me the data’ event), then react to it whenever the data is there. For that, our component code would need to be as simple as this:

export default class Product extends React.Component {
  // ...
  componentDidMount() {
    const { fetchProduct } = this.props;
    fetchProduct();
  }
  //...
  render() {
    const { productIsLoading, productData } = this.props;
    // ...
  }
}

And that means that… the actions should fetch the data? And reducers would dispatch actions? Sounds weird, right? Because it is. And it’s not what I’m suggesting whatsoever.

Enter the middlewares

To deliver the ideal data flow, redux middlewares are amazing. Let’s say we want our component to be exactly like that last bit of code. If we had something in between actions and reducers, for example, we could make this inbetween code deal with fetching the data and figuring out which reducers to fire, leaving redux to do its job of managing state and events, while leaving components focused on their jobs of rendering, and rendering only. The data flow would be something like this:

  • Component mounts and dispatches signal for redux to start the request
  • Redux's action describes success and failure reducers, along with which function will make the request
  • Middleware calls the request function, saves the promise and triggers the isLoading reducer
  • Component renders loading spinner
  • Middleware awaits the request and triggers the success or failure reducer based on the action's specifications
  • Success or failure reducer executes business logic and persists response's data on the redux's store
  • Component renders sucess or failure based on redux's data

With this proposed flow, our action could look like this:

export function fetchProduct(id) {
  return {
    types: {
      request: FETCH_PRODUCT_REQUEST,
      success: FETCH_PRODUCT_SUCCESS,
      failure: FETCH_PRODUCT_FAILURE,
    },
    apiCallFunction: () => fetch(\`/api/products/\$\{id\}\`),
  }
}

Ok, let’s slow down. What you see above is the current API for a request action, using the middleware included in our [react-redux-api-tools](https://www.npmjs.com/package/react-redux-api-tools) npm package. Dispatching this action would configure the middleware to make the request (by calling apiCallFunction) and to use the correct reducer whenever the request is done. Meanwhile, our component and reducers would remain unaltered:

export default class Product extends React.Component {
  // ...
  componentDidMount() {
    const { fetchProduct } = this.props;
    fetchProduct();
  }
  //...
  render() {
    const { productIsLoading, productData } = this.props;
    // ...
  }
}
export const productReducer = (state = initialState, action) => {
  switch(action.type) {
    case FETCH_PRODUCT_REQUEST:
      return {
        ...state,
        error: null,
        productIsLoading: true,
      }
    case FETCH_PRODUCT_SUCCESS:
      // here, you may execute any business logic
      businessLogic();

      return {
        ...state,
        error: null,
        productIsLoading: initialState.productIsLoading,
        productData: action.response.data,
      }
    case FETCH_PRODUCT_FAILURE:
      return {
        ...state,
        productIsLoading: initialState.productIsLoading,
        error: action.response.data,
      }
    default:
      return state;
  }
}

And voilá, the flow is much cleaner, simpler and decoupled!

If you liked this or it seems too magical...

Have you enjoyed this middleware API, want to develop your react-redux app like this or are just curious on how this all works? Then consider using and contributing to our react-redux-api-tools npm package. Bugs and feature proposals are welcome! And this is only the start; react-redux-api-tools has a bunch of other features I’ve not talked about. Give our docs a good read to find out how to further improve and simplify your code using our tools! Thanks, and see you later!