Using a global state manager and API calls and making these two synchronous is quite challenging. In total, it is possible to write code that works, but following the clean code principles is kind of a pain in the neck. Redux has the thunk middleware, which kind of handles the case, but other managers like context or others have some logic around each one that handles async actions. However, the main issue is that there is no concise and general way of separating API calls from business logic.
After struggling for a bit, I came up with some solutions about which I am not sure. What do you think is the best way?
Solution 1: Break each component into three parts following the Higher-Order Component (HOC) pattern. The outermost component handles API calls and dispatches the result to the store. The middle component is a container component that applies the business logic and provides the needed state and functions for presentation code. The innermost component holds the presentation logic and knows nothing about the business (dumb component).
// index.tsx
import ComponentAPI from './ComponentAPI'
export default ComponentAPI
// ComponentAPI.tsx
import ComponentContainer from './ComponentContainer'
export default function ComponentContainer (){
const getData = async () => Promise<somedata>
dispatch(setData(getData))
return <ComponentContainer />
}
// ComponentContainer
import ComponentView from './ComponentView'
export default function ComponentContainer (){
const state = useSelector(state => state.stateSlice)
const functions = () => {
dispatch(someAction())
}
return <ComponentView state={state} functions={functions} />
}
Solution 2: Create a custom API hook, either manually or with an API state manager like Tanstack Query. Return specific flags from this hook and use them to display different content for the user, such as "isLoading..." or similar messages.
// index.tsx
import ComponentContainer from './ComponentAPI'
export default ComponentContainer
// ComponentContainer
import ComponentView from './ComponentView'
export default function ComponentContainer (){
const state = useSelector(state => state.stateSlice)
const {isLoading, data, error} = useFetchData();
if(data){
dispatch(setData(data))
}
const functions = () => {
dispatch(someAction())
}
return <ComponentView isLoading={isLoading} state={state} functions={functions} />
}
Solution 3: Create a service class that includes properties and methods for making API calls. Utilize these methods within the container component and subsequently dispatch their results to the store.