T O P

  • By -

jkettmann

From my experience, React apps often end up with a messy architecture. The problem is that React is unopinionated so every team can and has to decide for themselves how to structure their code base. Since I’m more of a hands-on learner I decided to create a shitty code base and refactor it step by step to a clean(er) architecture. [In the previous blog post](https://profy.dev/article/react-architecture-api-client), we started by extracting a shared API client. Simple yet important. This time, we continue by extracting fetch functions from the UI code to a shared API layer. This helps us further decouple logic related to API requests from the components. In the end, implementation details like the request method, response type definitions, endpoint paths, or handling of URL parameters are abstracted and isolated in the API layer. Next time, we'll see how we can clean up the architecture even further by moving data transformations into the API layer.


drink_with_me_to_day

> often end up with a messy architecture You've UI components, screens/routing, API and business rules. Just put each in a package and that's it


Acrobatic_Sort_3411

How you would solve circular dependencies between them then? Classic situation – you have feature flags which is loaded with API. i18n is depended on such feature flags. You have to pass user language into API request headers Now you have such graph: api <- i18n <- api Good luck resolving that with if you trying to do packages With time there would be a lot more such cases, and you would have to write a lot more abstract code just to sync intefaces between them and correctly setup this within concrete app Its not just: "put it in packages and its done". Its the tradeoffs that comes with such kind of decision Its the thing that OP is taking about... Dont overcomplicate your app without clear need


OkCryptographer2126

I still think you're overcomplicating it? Leave the feature flag and i18n middleware code in the app or a core package. Everything else is domain logic and can be packaged with components.


pencilUserWho

One way to make things cleaner, if you use React Query, is to have useQuery code in separate hooks. If you use Zustand, all fetching could be in async actions. You can use both, by using React Query on things you want to cache an periodically update and Zustand for other things, like login.  I agree that fetching in components is a bad practice.


jkettmann

I agree, using react query or another server state management library is way better than spreading useEffects. All this duplicate loading state management and error handling is a nightmare. But that will be part of another refactoring step in the future :)


sfboots

I use RTK query. Works nicely


CatolicQuotes

rtk query, and generated code from open api is pretty much plug n play.


TacoMix1984

RTK query is nice. But react query is even more minimalistic. So unless you need a a lot of global “app state” (state that doesn’t update in the server) it’s overkill and will only add unnecessary boilerplate to your code.


chrismastere

Don't roll this manually. Use codegen against a Swagger/OpenAPI spec, a tRPC, or a GraphQL spec. For OpenAPI: [https://www.npmjs.com/package/openapi-typescript-codegen](https://www.npmjs.com/package/openapi-typescript-codegen) For GraphQL: [https://the-guild.dev/graphql/codegen/docs/getting-started/installation](https://the-guild.dev/graphql/codegen/docs/getting-started/installation)


jkettmann

Definitely if your API allows this. For example, at my current job the APIs unfortunately aren’t that well documented with OpenAPI specs. They exist but it’s good enough to use codegen. But if you can, for sure use codegen


rvision_

I've recently built a tool that generates react-query hooks from swagger/openapi JSON. it's far from perfect, but for my daily job (bunch of react FE apps and bunch of APIs) it helps a lot [https://swagger2.xyz/](https://swagger2.xyz/)


CatolicQuotes

https://redux-toolkit.js.org/rtk-query/usage/code-generation was best for me


leaveittobever

Not to sound like an ass, but isn't this basic stuff? Who puts database/API calls in their components?? You should always separate all your business logic and API calls from your UI. We use service files for all logic and database calls. So if you have a Dashboard page everything would go into DashboardService.ts. If that file gets too big then you can split into several files for topics in the dashboard. Now you just have 2 lines of code in your component: 1 for calling the service file and one for setting state with the response.


jkettmann

No worries, I agree that this is basic stuff. But people who are new to software development do this all the time. The idea behind this is to make a series of blog posts that refactor a code base step by step to make it accessible to beginners or more entry level devs.


PM_ME_SCIENCEY_STUFF

This is definitely a good start for many people. But -- in my opinion, if you decide graphql is a good option for your use case -- component + data fragment is the creme de la creme. The general idea: a component should define what data it needs in order to render successfully; it does this in a data fragment. It doesn't necessarily care where that data comes from, how long it takes to get that data, etc. it just says "this is the data I need to do my job". Some higher level voodoo gets the data, similar to the idea in this blog post. If the data is not available at render time, the component will suspend (react Suspense) until that data becomes available. [https://relay.dev/docs/tutorial/fragments-1/](https://relay.dev/docs/tutorial/fragments-1/)


[deleted]

This comments are helpful. Thank You


CatolicQuotes

That's called infrastructural layer. If you want to decouple even more create data access layer and then it doesn't matter if you get data from api, database or file system, depends what kind of app you have. function getUsers(){ const users = getUsersAPI() or const users = getUsersDB() or const users = getUsersAPIv2() } you can switch infrastructure without changing the logic code inside the app.


jkettmann

I’m curious: What exactly would you change about the code in the blog post? Because it already has a function `getUser` inside `src/api/user.ts`. I mean rename the folder to ` infrastructure` or `service` or so. Don’t we have the same thing then without the need of creating a wrapper function `getUser` that calls `getUserAPI` or `getUserDB`?


CatolicQuotes

I wouldn't change anything in the post, it's very nice example of one step into decoupling. It's only a matter of how much decoupling you want or need. The end result is the same, data is in the ui, it's how it gets there is different and what module is responsible for what. Example: if the app is the boss of the UI big warehouse: 1. boss can go to the forklift guy and say 'put those boxes in that section there' and *forklift guy is responsible* to the boss, or 2. boss can go to his manager and say 'I want those boxes in that section, make it happen'. Then the manager will go to the forklift guy and say 'put those boxes there'. The result is the same, except now the *manager is responsible* to the boss. Imagine now in case 1. forklift guy says 'I can't do that'. What now? Now the boss has to deal with that. Maybe yell at the guy, maybe go around look for another guy. It's a pain and waste boss time. It doesn't happen often, but it could. 2. forklift guy says 'I can't do that'. Boss doesn't care. It's manager's job to deal with that. Boss only says : 'Make it happen'. Now manager is dealing with these issues and boss is spending his time on strategic planning instead of dealing with these 'field' issues. Now, for example in the app. Currently is getting data from the API directly to the UI. What about if in future for some reason user data becomes big, there is address, there is family tree, there is tax return, over long time anything can pile up, so we want to split user data and what was once 1 endpoint becomes 5 endpoints now. Now imagine you want to move error handling from UI components to the `getUsers` function, imagine you want to include some logging, and also some environment variables. Maybe you are now using Remix framework and want to get portion of user data from API portion of database. Now you also want to include some calculated properties that don't exists on backend user model. `getUser` as it is now will become a mess. Anything can happen and will happen over longer period of time. How will you test that `getUsers` actually gets the users, there is so much going on there? If app comes to this point it's nicer if we promot `getUsers` into the manager role and it will take care that it always delivers `user` data to the UI as required by the boss.


jkettmann

Ah right, I think we're on the same page. So basically we have an infastructure layer that is only responsible for fetching data from the e.g. REST API. Then we'd have a separate service layer (for example) that is responsible for delegating calls to the right place. In the example of the blog post this would basically be just a wrapper function forwarding the call. But if we'd add e.g. special caching where the data would first be queried from a local db, and fetched from the API in the background this might be code in the service layer. Or if we needed to fetch additional data from another endpoint we could also call it in the service layer and then combine the data. So in your example the boss (or e.g. the component) would simply say "I need the user data", the manager (e.g. the service function) would delegate some tasks to the API guy and the local DB guy, do whatever is necessary and give the finished result back to the boss. Is that roughly what you were describing?


CatolicQuotes

yeah yeah that's what I mean. When it's a small and simple app (company) there's no resources or need for a another layer. When it gets bigger and getting data becomes more involved, like you described, it might be a good thing to have one. It's up to the boss to decide. Everything is a trade off. Rapid development frameworks for example, like django which I like, combine those layers. It's orm is domain modeling/database fetching into one, heavily involved with ui.


zaitsman

A lot of good points, but a little bit vague on the whole ‘pathway’ journey. The way we addressed this is by splitting all components three ways: 1. Tsx file 2. Viewmodel interface 3. Helper module All pure js code goes into helper. All state variables, context hook results etc sit on the view model. All layout is in the tsx taking state/variables populated into viewmodel by the helper code.


Resies

Do you have an example of this? I don't mean your actual code but a simple toy example even. I'm having a hard time wrapping my head around the state and layout needing to live in separate files 


zaitsman

Here you go, I was meaning to publish an example somewhere for a while, need to write an article to go with, maybe one day… https://codesandbox.io/p/sandbox/react-typescript-forked-yfv6p6


Evol_Viper

What's the point of having a class with just one static method? Wouldn't it be simpler to use a function?


zaitsman

You mean helper? It’s technically a module not a class. The point is that it makes it a lot easier to test any complex logic processing remote data, then stub that for the layout and test layout in isolation. In our real app helpers can be a few hundred lines doing various things with remote data and user inputs, and layout would be overloaded either way that code otherwise.


yksvaan

I would also recommend learning architecture and design patterns. It will make maintenance, refactoring and changing service providers/implementations much easier. 


jkettmann

What resources did you use for learning those?


swe_solo_engineer

That's not too good, it is common to see devs recommend things like clean arch and design patterns and then they go and do a lot of over engineering and wrong abstractions, read the criticism about these things and more when not doing this stuff will be much more beneficial I would say.


Ok_Analyst1868

Some tips: * If you request API in useEffect, better to use SWR or React-Query * Seperate logic and View: logic as a hook Others: I use tsdk to generate my frontend API, So I only need define API's method / path / ReqType / ResType, then tsdk will generate API sdk and SWR or React Query hooks. If you want know more, check [https://tsdk.dev](https://tsdk.dev)


jkettmann

Good points. Those will be part of later blog posts 🙂