top of page

How to Properly Architect Your React App

Atomic Components

For more detail, Click here.


Atoms are your smallest building block. These are simple UI elements, like inputs, buttons, text, etc… They are stateless and do not perform any function other than display and styling. These are only ever one HTML element. Here are a couple of basic examples.


Option.jsx



Button.jsx


Molecules are more complex. These are composed of one or more atoms and can sometimes hold local state. There is never any business logic here. Ever. Just event handlers. You can apply styles here as well if you so wish.


Select.jsx


Organisms are where you can start handling more complex logic. A credit card form with all the inputs or a header component are great examples. These will compose multiple molecules and atoms together. They can sometimes hold some local state, but it’s not advisable. We’ll get to state management real soon. I promise.


Ignore the useContext from this example for now.


Header.jsx


The eagle-eyed among you probably noticed the observer in that bit of code. I’ll go over the purpose of that in more detail later but the gist is that it wraps the component with a higher-order component (HOC) that provides reactability (is that a word?). If an observable property, in this case auth.loggedIn, changes its value, then it tells React to… well… react.


There are also templates and pages as well, but in practice this about as deep as you need to go with atomic design principles.

Now some non-react stuff, kinda…


Services

This should be the only place in your code that directly interfaces with your API or localstorage or websockets or whatever else external. They do not hold any state at all. Each service should act as a wrapper for specific business functions of your app. They’re classes that provide simple input/output functions for getting/fetching data.


A solid example would be a TodoService, which gets, sets, and deletes Todo’s via API calls. Another example is an AuthService, which handles login and registration. Here’s a stripped-down version of that:

AuthService.js


Models

Finally, this is where you store your state and provide functions for interacting with that state. Models should be the only place in your code where you will import your services. If you’re directly using services in a component, you’re doing it wrong. Also, Models are never used directly by a component, we’ll see how they’re used later.


There’s a bit to digest in the following example, no worries, I’ll explain everything right after.


Feel free to ignore the whole instance and private constructor part. This is just my way of making sure that the static propertyAuthModel.instance always returns the same instance, a singleton. This is really useful for models that are global, like Auth. But this is just a design choice that I made for this cherry-picked example I grabbed from an existing app.

AuthModel.js


You can see the introduction of some MobX mechanics above. Here’s what it all means.

@observable this is a property decorator that provides a way for us to assign special logic to a specific property of a class. This tells MobX that we care about this property and want to watch it for changes. Changes to the property will propagate through the app and trigger other MobX specific functions to run (if they use this property in any way), like computed getters.


@computed getters act as a sort of caching mechanism. In the example above the isTokenExpired getter will only be recalculated if one of the observable values used in its function body changes. If the return value from this getter changes then it will also propagate through the app and trigger other downstream changes. MobX is smart enough to forgo propagating changes if the return value is the same even if the observed properties used in the getter mutate.


@action is a method decorator. It’s a way to tell MobX that this method can and will mutate the value of an observable. There are some behind-the-scenes optimizations that MobX will do when multiple actions are invoked in rapid succession, like batching them all together. Strictly speaking, these are not required, unless you’re in strict mode ;)


Going back to the example of the Header organism from above. The observer HOC is what tells React that it needs to update when the return value from the loggedIn computed getter changes. That value comes from AuthModel (kind of, it passes through a ViewModel first, explained below), it will trigger React to update if the observabletoken changes OR if the return value from the computed getter isTokenExpired changes.


reaction is a function that takes in 2 functions as arguments. The return value of the first function is the observable value we want to watch. That value is then passed in as an argument into the second function, which is the side-effect we want to be run when that first value changes. This is easier to explain by example…

reaction


The first function there returns the this.token observable. If the value of this.token ever changes then we want to take that value and invoke the second function with it.


ViewModels

ViewModels encapsulate your business logic for a specific section/page of your app. It can hold some temporary UI state, like a loading flag or error message, but should mostly interface with Models for data.


There are a few ways to inject your ViewModel into components that need them. The first and easiest would be to have a container component that instantiates the ViewModel and then passes it down into view components using prop-drilling. This is fine as long as you don’t have very deeply nested components that will require direct access to the ViewModel.


The preferred method is using Reacts context API. I’ll explain it a bit but I suggest reading up on it if this is a completely new concept to you.

HeaderViewModel.jsx


AuthModel.instance is how we get our singleton. Again, this is a design choice for this specific Model and not a requirement for all models.


There’s two exports in this file, the actual HeaderViewModel and HeaderViewModelContext. The second one is how we can Provide the ViewModel to deeply nested components.

HeaderProvider.jsx


This is how we actually provide the context into Header and all of its children. We use theuseContext hook from React and pass in the HeaderViewModelContext as the argument to our ViewModel, which we just injected as the value to our Provider.


Here’s a slightly more complex ViewModel for comparison, LoginViewModel.

LoginViewModel.js


Here we can see that the login method will set an error message if the login fails. We can extrapolate and assume that the Login component will display that error if it’s set.


We can build this out a bit further and include a reaction to redirect the user to the dashboard route if the authModel token is set.



Although, this type of reaction is probably better suited to live in the AuthModel. In that case, we would be able to redirect to the login page when the token is unset.


In these two example ViewModels the Model that they both use was the AuthModel, which is a singleton and a special case. However, something like DashboardViewModel might need to instantiate new instances of AnalyticsModel and UserListModel (just some arbitrary Models that I made up).



Since the ViewModel is only ever created when the user is on the Dashboard page, then these two models are created alongside it. The two models will be re-created every time the user lands on the Dashboard, that means they’ll also be garbage collected once the user navigates away from Dashboard. That’s fine. It’s okay to get rid of state if it’s no longer needed.


Directory Structure

Here’s how all of the examples used so far would be organized. The files/folders marked with an * did not have examples but I put them in here for illustration purposes.

app/
  Header/
    components/
      HeaderBreadCrumbs.jsx*
      HeaderMenu.jsx*
      HeaderProgress.jsx*
    Header.jsx
    HeaderProvider.jsx
    HeaderViewModel.jsx
components/
  atoms/
    Button/
      Button.jsx
    Option/
      Option.jsx
  molecules/
    Select/
      Select.jsx
  organisms/*
models/
  AuthModel.js
routes/*
services/
  AuthService.js
utils/

app

This is where all the top-level route pages are kept. e.g. Dashboard, Login, Settings, etc…

This is also where layout components like Header, Body, and Footer live.


Each of these subfolders will most likely contain a components folder. This is where you would put components that are only ever used in that specific top-level component like HeaderMenu is only ever used in Header.


components

This is where you place all your reusable UI components. Most of your atoms and molecules will go here.


models

Pretty self-explanatory. All the models go here. Generally, models are used in many different parts of the app, so it wouldn’t make sense to place AuthModel in app/Header since app/Login might also use that model.


routes

I haven’t gone into routing. But this is where routing-specific files will go. I prefer to use router5. It’s worth looking into, I might do a post about this lib eventually.


services

Again, pretty self-explanatory. All your services should be placed here.


utils

All utils go here.


Conclusion

  • Keep your UI components as simple as possible (atoms)

  • Build up larger pieces of UI using atoms (molecules)

  • Create sections of the app using molecules (organisms)

  • Your view components should never handle any business logic

  • Providers instantiate ViewModels and provide them via the Context API

  • ViewModels instantiate Models

  • Models instantiate Services

  • Most of your state should be placed in a Model



Source: Medium - Vitaliy Isikov


The Tech Platform

0 comments

Recent Posts

See All

Σχόλια


bottom of page