This content originally appeared on Bits and Pieces - Medium and was authored by Alejandro Yanes
Structure a React app that can scale, using local state
I’ve wanted to write this article for some time now because most of the time I see a post about an “architecture”, it is using Global State Management (GSM) or using React state hooks in a manner that is too simple to use on a real-world app. I also think a good architecture should be based on concepts that allow a developer to understand it and fully implement it without thinking of it as just a set of rules to follow all the time.
So in this article, I’ll try to explain not only the code structure but also the concepts on which it is based, in hopes that it becomes easier to understand and possibly, change the way you think about coding. I’ll be using an e-commerce app as an example.
Previous knowledge
There are some concepts that you need to be comfortable with in order to understand this post. Some I will briefly explain and some I won’t. This is the list of the ones you should know beforehand:
- Promise and async/await
- TypeScript
- React hooks:
- useEffect
- useReducer
- Custom hooks
The why
I would like first to address the “why” I came up with this structure. I believe that GSM solutions offer a good architecture on paper since they provide separation between states logic and UI, but the truth is they are tools, not architectures, so there are no guides or principles on how to structure your code or how to compose your components.
In real life, this often leads to code being centralised in one place because the stores (or slices or however is named for a specific tool) are often created based on business entities instead of pages and what ends up happening is it can gather the logic from different pages, even though these may not be related, making the store harder to maintain.
For example, you would have a product store to manage all of the states related to the product entity, even though there might be several pages using product information. Technically you could create a store for every page and form thus reducing the amount of code by store, but this is not how this is usually done.
Another effect is that data access can become chaotic really fast since any component can access any state and can dispatch any action. This can make tracking the information flow tricky at best.
There’s also another issue about GSM: lingering states. This happens because the state lives outside the components and is not created or destroyed by them. This can cause some UX issues when entering a page and showing outdated data.
Lastly, we need to understand the way frontend apps work, contrary to servers that need to respond to multiple users at the same time, they’re used by one person only and that person is more often seeing one page, maybe one page and one form, so there’s no point on having states for every other page/form/component when they’re not being used.
The core concepts
The main concept on which this architecture is based is the Single Responsibility Principle from the SOLID principles. They are most often found on Object Oriented Programming documentation but can be applied to almost any thinking process.
This specific principle is the idea that an entity should have one and only one responsibility. It can be applied all through the development process, from how to structure components (which props to pass, which logic to move into a separate component…) to the functions that manage the state.
It also borrows from the functional programming paradigm the concept of Pure Function. A Pure Function is a function that always returns the same result if the same arguments are passed. It does not depend on any state, only on its input arguments (you can find more details on this topic here and here).
React components can be implemented using this idea, thus reducing complexity, coupling, and improving ease of testing. In a real-world application it’s impossible not to have states and side effects (which violates the pure function idea) but it’s worth keeping it in mind when structuring components and states.
The structure
The base of the structure is to have the states that belong to components inside the components, but at the same time, inside the component folder, the UI and state would be separate.
This folder tree describes the structure inside a page of your app. As you can see, there is a state folder with its own structure inside. It uses the useReducer hook from React to create even more separation of concerns by allowing the creation of a file basically for every step of the state management. Let’s analyse the files one by one.
The types
First, the state/types.ts, this one is really important to tie the states and the updates dispatched to mutate it
The State interface is self-explanatory. This example is small but it could hold more complex structures. The Update type is the one that defines all the possible updates to dispatch to mutate the state through the reducer function.
Finally, the CustomDispatch type creates the type to use for the dispatch object returned by the useReducer hook (it creates the type (value: Update) => void).
The reducer
The reducer function (inside state/reducer.ts) handles updating the state based on the possible updates dispatched, technically, it can be implemented in lots of different ways, but I chose the switch style because it makes type inference easier (to code) and this is an essential part to prevent bugs.
Using this way TypeScript is able to infer the type of payload when inside a case block, meaning I can only use update.quantity inside the case 'set_quantity' block.
The actions
Now let’s see how to implement an action. The format would be:
And this is what it would look like:
This pattern may seem complex at first, but it allows actions to be created on separate files and still get access to any state or outside variables, this in term makes testing easy, since it can be treated as a pure function and tested in isolation.
A point of confusion here can be which parameters to pass through the action generator and which the action needs to receive. A good rule of thumb for this is:
- action generator’s parameters are states and/or any data being used on the hook (fetched data, values from context…)
- action’s parameters are inputs from the UI
An example of the first point is the addToCart action defined above. As for the second point, let’s say you have a form that holds several inputs. This form is a controlled component and therefore needs an onChange action to receive the new values as the user updates the information. If we were to create this onChange the action generator would look like this:
Not all actions need to be coded this way. You might have actions like getCurrentCart, which are more like helpers and can be implemented/tested as simple functions. Regardless, all these functions are placed inside the actions folder to create a standardized file structure.
The connection point
Now let’s see how to tie it all together. This function goes on the state/index.ts file which is the custom hook that holds the state, defines the actions that will go to the UI and handles any side effects.
This bit is the standard way of using the useReducer hook from React. Typescript detects the types based on the reducer function and uses them for the state and dispatch values.
Finally, the return block is where actions are created. You can see that some are created inline like openModal: () => dispatch({ type: 'open_modal' }) and you can rightly point out that this does not follow the action file structure presented above, but given the complexity of this action I considered it a bit overkill to create a file for it.
This line, addToCart: addToCart(dispatch, state, product) is how to create actions defined using the action generator pattern.
This is how to connect the state hook with the UI component. This would be on the ProductDetails/index.tsx.
All this structure adds separation between the state and UI layers following the Single Responsibility Principle. For instance, the type of the addToCart action would be () => Promise<void>, meaning the UI would have no knowledge or concern as to what will happen when it is called.
This abstraction also makes testing easier since you can mock the state hook when testing the UI, thus abstracting from all the logic, the same way the state and actions can be tested without the need to render any UI.
As the last point, I want to mention that even though the hook has the word state on the name, the same as the folder containing it, it is meant to hold all the states, actions, data-fetching, and side effects needed for the component. This is done to avoid a structure so divided into little folders that it becomes too uncomfortable to read and use.
This will also create mental and real reference points for when you’re adding functionalities or hunting bugs.
Some remarks
Now that you’ve seen the code let’s get back to the theory.
Where to place the state?
A common concern with this architecture would be where to place the state. There is no strict rule here as it depends on the requirements but as a rule of thumb, I would say to place the state as close to the component as possible, and only lift it up if it needs to be shared or for some business requirement (usually UI). This piece on the React documentation has a good explanation of this process.
Performance
You can think that this way of structuring the states can have some performance issues and you would be right, sort of.
Having the state at the top of the page can be problematic if you have groups of states that only affect one part of the page since updating any of those states will trigger a page-wide re-render, but if you think about it, the same can be said of any GSM solution. So how do they solve it? Well, they optimize beforehand.
Redux toolkit uses selectors (using the reselect npm package) to access states, and these can be coded to be as broad or precise as you want them to be, but there’s no magic behind it, they simply use memoization techniques to prevent re-renders, which I am not saying it’s a bad approach, just that it is a thing you can also do with the tools that React provides.
It's also worth noting that when memoizing you’re trading computational time for memory space, so over-optimizing beforehand might not be what you want. A rule of thumb would be to first code your states and UIs and then measure the performance to find the bottlenecks and optimize accordingly.
Rule of imports
This rule is meant to help when structuring components, deciding which are common and which belong inside a component. The first thing to know is that folders are classified into two types, grouping and module folders. You can use the folder structure at the end of this section to understand better how to use this rule.
Grouping folders, as the name says, are just meant to group similar types of files, making the structure cleaner. Some examples are:
- src/api
- src/helpers
- src/components/base
These folders don’t hold any implementation inside so importing directly from them would not work. Technically you could add an index.ts file to gather all the exports from the files inside and export them, but that would not make the folder a module folder.
import ... from 'src/api'; // this would not work
On the other hand we have Module folders. These folders do contain implementation details inside. These are usually component or state folders. Some examples of this type of folder can be:
- src/components/base/Button
- src/components/layout/PrivateLayout
- src/components/page/ProductDetails
Now that you know the types of folders, here’s the rule:
“You can import from files inside grouping folders but not module folders”.
The idea behind this is to avoid “borrowing” logic from other components without making it clear that that logic is used in some other places.
When working with Node.js modules API any file is considered one, and you control what is exposed or not via the export keyword.
This makes it impossible to control exports at the folder level since having a index.ts|js file won’t stop anyone from importing directly from the file they want.
This is why having a rule limiting how to import can be useful when structuring your code.
It also helps to see the folder tree as a hierarchy tree, where the higher the function/component is the more common to the application it is and vice-versa the deeper it is the more specific it is.
If a function/component is meant to be used by several other entities it should be at the same level or higher than the files that import it.
Prop drilling
Another issue can be prop drilling, which is basically when you have to pass down props from one component to its children for several levels. This piece from the React documentation gives a good explanation and here are some good pointers on if and when to use context, which is the common way of solving this problem.
If you decide that you indeed need to use context here are my suggestions. First, move the types.ts file out from the state folder (following the “rule of imports”).
When creating a context consider passing only the states you need, it can be tempting to simply pass the whole state and actions but you will be passing down useless information that only adds to the clutter. Also, consider creating custom hooks to access the context data instead of exporting the context object.
Lastly, keep in mind that the purpose of your context is to pass the information along, so there should not be any states definition inside of it.
Now let’s see what it could look like. Let’s say you want to pass the product, quantity and functions to update it.
Create a type to represent what information will be passed through.
Then create a context.tsx file.
This is what it would look like when using the provider.
Conclusions
In this article I’ve shown you my take on a React application architecture, from the whys to the hows. I really hope you have found it interesting. If you have any questions, suggestions or corrections let me know in the comments. I’m looking forward to your feedback. Thanks!!
PS: Here’s the code repo on GitHub: https://github.com/AlejandroYanes/bazar-web
Build apps with reusable components like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.
Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.
Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:
→ Micro-Frontends
→ Design System
→ Code-Sharing and reuse
→ Monorepo
Learn more
- How We Build Micro Frontends
- How we Build a Component Design System
- The Bit Blog
- 5 Ways to Build a React Monorepo
- How to Create a Composable React App with Bit
How to Build a Scaleable React application was originally published in Bits and Pieces on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Bits and Pieces - Medium and was authored by Alejandro Yanes

Alejandro Yanes | Sciencx (2022-10-11T06:03:11+00:00) How to Build a Scaleable React application. Retrieved from https://www.scien.cx/2022/10/11/how-to-build-a-scaleable-react-application/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.