DEV Community

Michael Wolf Hoffman
Michael Wolf Hoffman

Posted on • Originally published at codewithwolf.com

What To Do With Shared And Complex Logic in React Applications

At my full time job, I write a lot of vue. For my hobby projects outside of work I like to switch back and forth between UI frameworks (or using none at all).

Lately, I have been playing with React and mostly enjoying my time with it.

But I did come across a common problem recently that took me a while to come up with a viable solution.

The Problem: Shared Complex Logic

What should I do with my more complex UI logic in react?

I had asked a few talented developers that use react I know. Some of them said that they will write that shared logic in custom hooks.

I thought that was a great idea, but hooks are only for functional components. That doesn't help with anyone using class based component.

So I think I came up with a pretty slick way to handle this for ALL react apps and ALL react components (functional AND class components). It would work with or without redux, mobx and requires no library.

What is Shared Complex Logic?

By this, I mean API calls, async tasks, presentational logic shared across multiple components, etc.

Business logic is typically going to live in the API's business layer.

And while some front-end applications will never face this issue, UI presentational logic can at times get quite complex.

As your app grows, your business requirements evolve and become more complicated you need a place to hold that logic.

Can I Put The Logic in Action Creators?

The first thing I tried was putting this logic in action creators.

I have found that redux action creators do NOT handle logic well.

I would add small amounts of logic to a redux action creator. If I was using redux-thunk and making an API call. This worked great if I had one action to return for a successful request and another one for a failed request.

But if my action creator had to make an API call and then based on the response, complete more logic before it could return an action, then things got bad fast.

So no, action creators really are not good for processing more than simple logic.

Solution

A solution to my problem that I discovered is to place complex logic, api calls, shared code, and whatnot into a service (or services).

You can then create a new instance of the service or services and use React's Context API to give access to these services to the component.

Example

Let's go through a simple example.

Create a Service

We will need a service so let's create something simple. We can have a services folder in the src directory and we can call our service data.service.js

// src/services/data.service.js

export default class DataService {
     getSomeData() {
         return new Promise((req,res)=>{
               return setTimeout(()=>{
                   return ["Foo", "Bar", "Baz"]
             }, 1000);
         })
    }
}
Enter fullscreen mode Exit fullscreen mode

Above we just have a very simple service that we are going to sort of fake an async request and return an array of strings.

Create the Context

We can now create a context folder in the src directory to hold our contexts.

Then I created a context/services.context.js file.

I like to keep this in it's own file to prevent any possible circular dependency issues.

All you really need in there is to create the context and export it.

// src/context/context.js

import React from "react";

export default React.createContext({});

Enter fullscreen mode Exit fullscreen mode

Provide the Context

Now we need to provide our ServiceContext so it can be consumed by the children components.
In this example I am doing this in the App.js, but this approach could be modularized and placed in a parent component elsewhere and only accessible to that component's children too.

import React from 'react';
import DataService from './services/data.service';
import ServiceContext from './context/services.context';

class App extends React.Component {

  render() {
    return (
        <ServiceContext.Provider
          value={{
            dataService: new DataService()
          }}
        >
          <App />
        </ServiceContext.Provider>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

In our App.js we are importing our ServiceContext and DataService. We then pass the ServiceContext.Provider a value with the new DataService instance so it can be used in the context.

Consume the Context

Let's create an example component so we can consume the context.

//  src/components/example.component.js

import React from 'react'
import ServiceContext from '../context/services.context';

export default class ExampleComponent extends React.Component {

  static contextType = ServiceContext;

  async componentDidMount() {
    console.log(this.context);
    let data = await this.context.dataService.getSomeData();
  }

  render() {
    return (
      <div>
        <h1>
            Welcome To My Example Component!
        </h1>
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

You would consume this context just like you would any other.

In this component we set the static contextType property to the ServiceContext that we imported.

Then, we can call our DataService's getSomeData method that we created earlier.

Conclusion

Where is a good place to keep the more complex logic that is shared across multiple components in a react application?

My first thought was in Action Creators while using Redux.

But I quickly discovered that redux action creators are not great at completing complex tasks. As your business and presentational logic evolves, you will need to store this logic somewhere.

I found that creating a service and using the Context API is a great solution.

This works regardless of using redux, mobx, or any other libraries. No library prevents your ability to do this, and it requires no libraries other than React to implement.

This approach is flexible. You services contexts can be scoped to any module or it can be global in the App.js.

This approach is also loosely coupled. If you need to overhaul a service or remove it completely, that can be done easily.

While the Context API can have performance drawbacks when holding significant amounts of state because of how it re-renders the DOM on updates, this approach does not have these issues because the services are initialized once when the app is loaded and are not updated again.

Discussion (0)