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.
someFunc
does: Declare it outside the component. – Diversion