Reactjs separation of UI and business logic
Asked Answered
A

4

54

I am new to react and I find it sore in the eyes to look at the component flooded with lots of functions and variable initializations together with the UI. Is it possible to separate them?

Instead of the default setup, like below. How do I separate the business logic into another file?

function MyComponent() {
    const [data, setData] = useState('');
    const someFunc = () => {
        //do something.
    };
    ... some 100-liner initializations

   return ( 
       ...
   )
}
Adolfo answered 26/9, 2021 at 7:57 Comment(5)
Without knowing what someFunc does: Declare it outside the component.Diversion
what I meant by separating them is putting all the business logic inside another js file. @FelixKlingAdolfo
Make a parent component which should have the business logic, the UI should be in another child component. Call the child in the parent passing down all the required props.Lycanthropy
You can do that too. developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/ModulesDiversion
Take a look at this dev.to/mrdulin/react-redux-application-architecture-5f8fSulphate
C
96

Yes it is possible, That is called as Separation of concern.

You can create your component structure as below.

MyComponentDirectory
 - useCustomHook
 - Component
 - helper

The code will look like this one.

Hook

const useCustomHook = () => {

    const [value, setValue] = useState('');
    const handleClick = (value) => {
        setValue(value)
        //do something.
    };
    ... some 100-liner initializations/business logic, states, api calls. 

    return {
        value, 
        handleClick,
        ... // Other exports you need. 
    } 
}

export default useCustomHook; 

Component

function MyComponent() {
   const {
       value, 
       handleClick, 
       ... // Other imports 
   } = useCustomHook() 

   return ( 
       <Element value={value} onClick={handleClick} />
   )
}

Helper

const doSomething = () => {}

EDIT

Here's a detailed example of React counter application using Separation of concern

Structure

Directory
- App
- Counter
- useCounter
- helper

App Component

import Counter from "./Counter";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <Counter />
    </div>
  );
}

Counter Component

import useCounter from "./useCounter";

const Counter = () => {
  const { count, increaseCount, decreaseCount } = useCounter();

  return (
    <div>
      <p>{count}</p>
      <div>
        <button onClick={increaseCount}>Increase</button>
        <button onClick={decreaseCount}>Decrease</button>
      </div>
    </div>
  );
};

export default Counter;

useCounter Hook

import { useState } from "react";
import numberWithCommas from "./helper";

const useCounter = () => {
  const [count, setCount] = useState(9999);

  const increaseCount = () => setCount(count + 1);
  const decreaseCount = () => setCount(count - 1);

  return {
    count: numberWithCommas(count),
    increaseCount,
    decreaseCount
  };
};

export default useCounter;

Helper Function

const numberWithCommas = (x) => {
  return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};

export default numberWithCommas;

Here's the working example in Codesandbox

Note: if you create a simple Javascript util function instead of hook then you won't be able to access other hooks, context inside that function.

Coopersmith answered 26/9, 2021 at 8:44 Comment(8)
I prefer this answer than below since it does not mess up the states.Adolfo
Updated the example with Real React counter and working example at Codesandbox.Coopersmith
This is amazing example! Upon further thinking, we could move numberWithCommas(..) method out of custom hook and directly call in ui <p>{numberWithCommas(count)}</p> This way our custom hook acts as isolated model (data) without any view logic. All it needs to do is persist state, update & notify listeners (UI). That makes it more re-usable and clean. Just a thought :)Esemplastic
Great idea! We can surely do that. This was added just to showcase how we can use helper within the custom hook to seggregate any data manipulation from react component.Coopersmith
Great answer! Love the clear separation of concerns.Gann
learning react after working in Angular for years and it is really frustrating to have all the rendering and component logic in one place and that too html(jsx) below a ton of business of logic. This is exactly what i was looking for. I think this should be default convention that every component should be have a hook for business logic keeping the UI component lean.Schulz
great example, I was using customHooks for different purposes but it's a helpful approach for separating logic.Invagination
it is error-prone, repetitive, and boring to define variables and set them again in return in the Hook especially when you have a lot of statuses, isn't there something better? like exposing every variable here? or use a class/object?Pogrom
S
5

A common approach that I use myself is to separate the business logic into its own file myComponentHelper.js

This will also make it easier to test the function because it will not be able to use and change the react state without having it passed in as arguments and returning the changes.

myComponent/
  myComponent.jsx
  myComponentHelper.js
  myComponentTest.js
// myComponent.js

import { someFunc } from './myComponentHelper';

function MyComponent() {
    const [data, setData] = useState('');
    
    const x = someFunc(data);

    return ( 
        ...
    )
}
// myComponentHelper.js

export const someFunc = (data) => {
    //do something.
    return something;
}
// myComponentTest.js

import { someFunc } from './myComponentHelper';

test("someFunc - When data is this - Should return this", () => {
    const data = {...};
    const result = someFunc(data);
    expect(result).toEqual("correct business data");
});
Semiliquid answered 26/9, 2021 at 8:20 Comment(2)
Can you give a basic implementation?Adolfo
Yes. Here is a basic implementation. codesandbox.io/embed/…Semiliquid
Z
2

Separating business logic into other files can be done in various different ways.

  1. Create a helperFile.js that has logic or basically the functions required by the corresponding file.
  2. Creating Custom Hooks. More on that can be found here in the official docs or in this playlist (refer the videos at the very end)
  3. Global State mangement way - where contextAPI or Redux is used to seperate out state and business logic
Zena answered 26/9, 2021 at 9:12 Comment(0)
A
0

To adhere to the single responsibility principle, it's beneficial to streamline your business logic using straightforward custom hooks. You can employ prototype inheritance, classes, or mixins where necessary. For managing API calls, consider using Tanstack Query, and for global state management, Jotai (Atoms) is highly recommended. For managing complex forms with good performance many people use React-hook-form. These libraries are not only easy to grasp but also simplify maintenance significantly.

Gone are the days when you needed to write extensive Redux boilerplate code including actions, reducers, and stores. Modern tools like Redux Toolkit are powerful, but may not always be necessary depending on your stack.

Below is a simplified example with Declarative programming:

// counterFunctions.js 
// @todo: create a 'counterFunctions.test.js' file for unit testing
const counterFunctions = {
  increment: (counter) => ({ ...counter, value: counter.value + 1 }),
  decrement: (counter) => ({ ...counter, value: counter.value - 1 }),
  reset: (counter) => ({ ...counter, value: 0 }),
};

export default counterFunctions;

// useCounter.js
import { useState, useCallback, useMemo } from 'react';
import counterFunctions from './counterFunctions';

const useCounter = ({ counter, setCounter, onSubmit }) => {
  const incrementCounter = useCallback(() => {
    const newCounter = counterFunctions.increment(counter);
    setCounter(newCounter.value);
  }, [counter, setCounter]);

  const decrementCounter = useCallback(() => {
    const newCounter = counterFunctions.decrement(counter);
    setCounter(newCounter.value);
  }, [counter, setCounter]);

  const resetCounter = useCallback(() => {
    const newCounter = counterFunctions.reset(counter);
    setCounter(newCounter.value);
  }, [counter, setCounter]);

  const submit = useCallback(async () => {
    await onSubmit(counter);
  }, [onSubmit, counter]);

  const isEnableSubmit = useMemo(() => {
    // Replace with your complex logic
    return counter > 0;
  }, [counter]);

  return { increment: incrementCounter, decrement: decrementCounter, reset: resetCounter, submit, isEnableSubmit };
};

export default useCounter;

// Counter.js
import { useAtom } from 'jotai';
import { useCounterAPI, useCounter } from './customHooks';

const Counter = () => {
  // Data layer
  
  // TanstackQuery mutation custom hook
  const { queryHelper, sendCounterData } = useCounterAPI();
  // Jotai atom for global state
  const [counter, setCounter] = useAtom(counterAtom);

  // Business layer (Custom hooks)
  // NOTE: the variable 'isEnableSubmit' has
  // very complex if-else logic hidden inside the hook and
  // not pollutes our components.
  // No nested ternary operator here.
  const { increment, decrement, submit, isEnableSubmit } = useCounter({
    setCounter,
    onSubmit: sendCounterData,
  });

  // View layer with improved error and loading handling
  return (
    <>
      {counter}
      <Button onPress={increment}>Increment</Button>
      <Button onPress={decrement}>Decrement</Button>
      <Button
        onPress={submit}
        disabled={!isEnableSubmit || queryHelper.isLoading}
      >
        {queryHelper.isLoading ? 'Processing...' : 'Submit'}
      </Button>
      {queryHelper.isError && (
        <div>Error occurred: {queryHelper.error.message}</div>
      )}
    </>
  );
};

Note: The React team encourages the use of custom hooks over higher-order components (HOCs) for managing business logic. https://www.youtube.com/watch?v=dpw9EHDh2bM

In the evolution of React best practices, we've moved from React mixins to Redux reducers to HOCs, and now to React hooks.

Bonus: Writing unit tests for the custom hook 'useCounter' is straightforward.

In summary:

  • Embrace modern libraries like Tanstack Query and Jotai for state management.
  • Uphold SOLID principles, clean code practices, and maintainable architecture.

This approach ensures your code remains efficient and scalable without unnecessary complexity.

Airla answered 3/4 at 7:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.