React Project Architecture
Ezran Bayantemur
Posted on October 4, 2021
I’ve been developing applications with React for a long time and I’m loving it more and more. React is such an amazing library for creating application architecture and it’s plan. It’s offering the opportunity of applying basic software principles (like SOC , like SOLID ..) on our project and keeping codebase clean even if our project scale grows. Especially after hooks it’s became so yummy!
In this article I wanted to talk about how can you create project structure and architecture with React. You can think it’s will be mixed article of best practices and React basics. Of course they are not “rules” or something else, you can go on however you want, I just want to light some blub on mind :)
It will be a little long article but I think it will be helpful.
In addition; I’m gonna give examples on React Native but you can think exactly the same struct on web, ReactJS.
If you ready, let’s go! 🤟
Originally I wrote this article on Medium and I'm planning to share some article and translate my old Turkish articles for you people. If you wanna support me you can give me clap on there and keep me rock!
Navigation
Navigation is the backbone of the application. The cleaner and the balanced you keep it, that much easy to integrate when new requirements, new pages comes and that much less time to spend for “Where and how I’m gonna implement the new changes?” question.
When you developing an application, all the project architecture is reveales on the design phase. All the questions like; Which screens will gonna be? Which purpose will it serve? How will the pages be grouped in the application? finds their answers and; at this point, you can create the navigation architecture. You can create entire architecture with looking at the screen designs.
If your application has screens with different purposes you can gather them on a separate Stack architecture. For example, if application has main modules like profile, messaging, timeline;
- App
- ProfileStack
- MessageStack
- TimeLineStack
...
...
...
- ProfileStack
- ProfilePage
- UpdatePreferencesPage
- AddNewPhotoPage
- MessageStack
- InboxPage
- NewMessagePage
- TrashCanPage
- TimelineStack
- TimelinePage
- PostPage
- CommentsPage
- LikesPage
you can create a structure something like that.
Main navigator has Profile, Message and Timeline stacks. In this way, main modules of our application are certain and they have separated sub screens.
For example; MessageStack module is related only to messaging section and tomorrow, if it need any new screen, updating only that section will do the work. We can navigate from any screen to anywhere. react-navigation gives us the limitless freedom about it, only we should do our planning well.
The is no limit to nested stacking. Modules with has similar context can gather to same stack structure. For example; if notification page in settings section holds 3 of 4 page; you can gather them on the same Stack. Because seeing the pages with NotificationPreferences, NotificationDetail, BlockedAppNotifications names on the SettingsStack is not well-doing thing. They sounds like they need Notifications stack. Besides, placing them like this, means we will gonna implement every new page with same navigation idea. After all we should stick on a certain development method, right? What if tomorrow 10 paged modules comes?
A project dies because of not following certain development way or following the wrong development way.
Components
When you develop a module, complexity feel structures or open for reusability structures should be design as separate components.
While developing a page or module with React, always consider to divide. React gives you this opportunity and you should use it as much as you can. Your current component may look simple today, you may not think to divide it but the person who gonna develop it after you, if keep developing it like that and if that component grows like 200–300 loc (line of code), revise it will took much more time than develop it.
It’s like the toilet, you should leave it like you wanna find it.
Then, when should divide a component?
While creating a design of an app, a fixed design principle is select for appealing to the eye. Buttons, inputs, modals has always a consistent design and looks like each other. Instead of ten different button design you’d see ten different variation of one button. This is consistency, it creates signature of application on users eye memory and you’d (actually, you should) create your consistent component structure while these looking at designs.
For example; if there is a button design which is using so frequently, you can create it’s variation and store it on general component directory. Also you can store on the same directory the components which is not using anywhere else but smells like reusable.
But, if there is a component which using only one screen, it’s better to be store it on the same directory with the related screen. Let’s give an example;
If graph and table components will gonna use only and only by analysis screen and if it will gonna completely stick by analysis logic, then it’s better be keep it on the same directory. Because the modules are which needs each others should be close to each other. But in that example, list modal and button components can be store on general components and call from there. They created because of that.
Then, our file directory will gonna be like;
- components
- Button
- Button.tsx
- Button.style.ts
- Button.test.tsx
- Button.stories.tsx
- index.ts
- ListModal
- ListModal.tsx
- ListModal.style.ts
- ListModal.test.tsx
- ListModal.stories.tsx
- index.ts
...
...
- pages
- Analyze
- components
- AnalyzeGraph
- AnalyzeGraph.tsx
- AnalyzeGraph.style.ts
- AnalyzeGraph.test.tsx
- AnalyzeGraph.stories.tsx
- index.ts
- AnalyzeDataTable
- AnalyzeDataTable.tsx
- AnalyzeDataTable.style.ts
- AnalyzeDataTable.test.tsx
- AnalyzeDataTable.stories.tsx
- index.ts
- Analyze.tsx
- Analyze.style.tsx
- index.ts
that.
Components that are related to the analysis module and will only serve it are locating near that module.
Note: While naming, giving the related module name as prefix is much better choice, I think. Because you may need another graph and table component on completely different module and if you give just DataTable as the name, you may have ten different DataTable component and you can have some hard time to find which component using on which module.
A second way: styling stage
The most main base principle of writing clean code is giving the right name to variable and values. Styles are our values too and they should naming right. While writing a style for a component, the more you give right names, the more you write a maintainable code. Because the person who will continue to develop it after, will be easily find which styles belongs to where.
If you use same prefix so frequently while naming the styles, then you should consider that part as another component.
So if your UserBanner.style.ts file looks like that;
contanier: {...},
title: {...},
inner_container: {...},
avatar_container: {...},
avatar_badge_header: {...},
avatar_title: {...},
input_label: {...},
you may feel you need a component like Avatar.tsx. Because if there is a grouping while styling stage then it’s mean a growing structure coming. There is no need to repeat 3 or 5 times for consider a structure as an another component. You can follow it while coding and make inference.
In addition; there is no rule to all component should have logic. The more you divide the module, the more you control it and more you can write tests.
Let it be a little road tip 🧳
Hooks
Structures which are plays a role on lifecycle and represents a work logic, should be abstract as a hook.
For that, they need to have their own logic and as like on the definition, they should be on the lifecycle.
The main reason of it is the reducing the work weight on general structure and creating reusable work parts. Just as we create custom components for reducing code complexity; custom hooks can be create for the same way. Important thing is being sure of created structure and it working correctly.
How we understand that we need a custom hook?
Let explain it with an example;
Think that you need a search structure on project scope. You need a SearchBox component which will gonna be able to usable from everywhere and using fuse.js package for search action. First, let’s implement search structure to two example component.
(I didn’t keep codes too long but you can think that three point sections are own parts of the component)
function ProductPage() {
const fuse = new Fuse<Product>(data, searchOptions);
const [searchKey, setSearchKey] = useState<string>("");
const [searchResult, setSearchResult] = useState<Product[]>([]);
...
...
useEffect(() => {
if (!data) {
return;
}
if (searchKey === "" || typeof searchKey === "undefined") {
return setSearchResult([...data]);
}
const result = fuse.search(searchKey);
if (!result) {
return;
}
setSearchResult(result.map((r) => r.item));
}, [data, searchKey]);
...
...
function search(pattern: string) {
setSearchKey(pattern);
}
...
...
return (
<Layout>
<ProductSearchBox onSearch={setSearchKey} />
<ProductInfo />
...
...
<View>
<ProductDetail />
<List data={searchResult} item={ProductCard} />
</View>
...
...
</Layout>
);
}
export default ProductPage;
function MemberPage() {
const fuse = new Fuse<Member>(data, searchOptions);
const [searchKey, setSearchKey] = useState<string>("");
const [searchResult, setSearchResult] = useState<Member[]>([]);
...
...
useEffect(() => {
if (!data) {
return;
}
if (searchKey === "" || typeof searchKey === "undefined") {
return setSearchResult([...data]);
}
const result = fuse.search(searchKey);
if (!result) {
return;
}
setSearchResult(result.map((r) => r.item));
}, [data, searchKey]);
...
...
function search(pattern: string) {
setSearchKey(pattern);
}
...
...
return (
<Layout>
<MemberSearchBox onSearch={setSearchKey} />
...
...
<View>
<Header />
<List data={searchResult} item={MemberCard} />
</View>
...
...
</Layout>
);
}
export default MemberPage;
When we look at to our components the main thing that we notice there is the same search structure were implemented and clearly code repeat can be seen. If there is so much code repeat on a structure, that means something going wrong out there.
In addition to that; when someone opens any file, it gonna want see only and only filename related code. When you open CommentsScreen.tsx file, you wish to see just comment related codes, not any other grouped logic. Yes, on the example our search structure is related with Product and Member components and they working for them. But they represent a their own logic from now and furthermore, they can be convert reusable structure. Because of that we need custom hook or component structures.
Back to example; there is a clearly use of state for search action and it’s takes a place on the lifecycle. When user start to type to the search input, that string storing on the searchKey state and when it takes update main list also filtering too.
So how we can design it much better?
We can gather our search structures on a hook named useSearch. We should create such a hook that is not dependent to any module and has reusable structure for freely use anywhere.
Because we’ll gonna use fuse.js for search, we can send out data and search criteria as input and we can return search result and search function which will gonna trigger later.
Then, the hook that we gonna create is;
interface Props<T> {
data?: Readonly<T[]>;
options?: Fuse.IFuseOptions<T>;
}
interface ReturnType<P> {
search: (s: string) => void;
result?: P[];
}
function useSearch<K>({data, options}: Props<K>): ReturnType<K> {
const fuse = new Fuse<K>(data || [], options);
const [searchKey, setSearchKey] = useState<string>('');
const [searchResult, setSearchResult] = useState<K[]>(data || []);
useEffect(() => {
if (!data) {
return;
}
if (searchKey === '' || typeof searchKey === 'undefined') {
setSearchResult([...data]);
return;
}
const result = fuse.search(searchKey);
if (!result) {
return;
}
setSearchResult(result.map(r => r.item));
}, [data, searchKey]);
function search(pattern: string) {
setSearchKey(pattern);
}
return {search, result: searchResult};
}
export default useSearch;
will be this.
With TypeScript support our hook can be used with types. With that we can send and receive any type while using it. Work flow inside hook is same as we talked before, you’ll see when check out the codes.
If we want to use it on our components;
function ProductPage() {
const {result, search} = useSearch<Product>(data, searchOptions);
...
...
return (
<Layout>
<ProductSearchBox onSearch={search} />
<ProductInfo />
...
...
<View>
<ProductDetail />
<List data={result} item={ProductCard} />
</View>
...
...
</Layout>
);
}
export default ProductPage;
function MemberPage() {
const {result, search} = useSearch<Member>(data, searchOptions);
...
...
return (
<Layout>
<MemberSearchBox onSearch={search} />
...
...
<View>
<Header />
<List data={result} item={MemberCard} />
</View>
...
...
</Layout>
);
}
export default MemberPage;
As it can see from now search structure is abstracted from the components. Both code complexity is reduced and whenever we need a search structure we have a custom hook on our hands.
With that we created a much more clean and testable structure.
By the way, like I said; hooks can be create for dependent on a context or generic use as like as components. On that example we created custom hook for general use but we can create custom hooks for specific job or context. For example for data fetching or manipulating on specific page you can create your own hook and abstract that job from the main component.
I mean;
- hooks
- useSearch
- useSearch.ts
- useSearch.test.tsx
- index.ts
...
...
- pages
- Messages
- hooks
- useMessage
- useMessage.ts
- useMessage.test.tsx
- index.ts
- useReadStatus
- useReadStatus.tsx
- useReadStatus.test.tsx
- index.ts
- Messages.tsx
- Messages.style.tsx
- index.ts
While useSearch using on the project scale; useMessage is responsible for data fetching, useReadStatus is using for subscriber read status on a message. Same logic as on the components.
And that’s Hooks 🔗
On the Clean Code book; there is such a good description for single-responsibility requirement of a function. If we use it for React: A component or a hook should do one thing, should do it only, and should do it well.
Context
You should create different context structure for the modules which can’t communicate directly but connected from the content.
Context shouldn’t be considered like “all the wrapper around entire project”. When complexity of project increase; structures which has connection with logic are increase in number too and these parts should keep separated from the each other. Context takes the role of communication between these parts. For example; if you need communication in components and pages on the messaging module; you can create MessagesContext structure and create independent work logic by wrap it to only messaging module. In the same app if you have Nearby module which you can find friends around you and if it has numerous work parts; you can create NearbyContext and abstract it from the others.
So, if we need a structure like, global, accessible on anywhere; can’t we wrap main app with a context?
Of course you can.
That’s why global state management stands for.
On this point the main thing that you should be careful is not to overload a context. You shouldn’t wrap the app with only the AppContext and put all the states like user info, style theme and messaging. Because you’ve already create work modules for them and can clearly see they are different structures.
In addition; context updates every component which connected to it on any state update.
In example; you’ve created member and messages states on AppContext and you listen only member state on Profile.tsx and only messages state on MessageList.tsx component. When you receive a new message and update the messages state; Profile page will take the update too. Because it listen the AppContext and there is a update on the context which is related (which is actually not). Do you think there is a really relation between messages and profile modules? Why there is an update should be happen on the profile section when new message comes? That means an unnecessary refresh (render, update, however you want name it) and when they grow like an avalanche, they will cause so much performance problems.
Because of that reason you should create different context for different work content and keep safe the entire logic structure. Even a more reason; when the application take a step to the maintenance phase, the person who will care the update on any module, should be able the select related context easily and understand the architecture with no pain. Actually right here the most base teaching of clean code principle comes into play again; the right variable naming as we just mentioned.
When you naming your context on the right way, your structure will be go on healthy too. Beacuse the person who sees the UserContext will be know that it should take or put the user info from here. It will know not to manage the works about settings or messaging from the UserContext. Because of this, the clean code principles are really important discipline.
Also, users have opened an issue about Context API back before and they wanted; components which are listens states from the Context, should take refresh only when the subscribed states updated, just like Redux. This answer of Dan Abramov is actually summarizes the working logic of the Context API very well.
A component which listens a Context should must need that Context. If you see an unnecessary state which you called from a Context; this mean either this state has no place on that Context or you set that Context structure wrong. It’s all about the architecture that you created.
While using Context, always be sure for that your components really need the states which are you called. You will be less likely to make mistakes.
For a little example;
[ App.tsx ]
<AppProvider> (member, memberPreferences, messages, language)
<Navigation />
</AppProvider>
If we separate;
[ App.tsx ]
<i18nProvider> (language)
<MemberProvider> (member, memberPreferences)
<Navigation />
</MemberProvider>
</i18nProvider>
...
...
...
[ MessageStack.tsx ]
<MessagesProvider> (messages)
<Stack.Navigator>
<Stack.Screen .../>
<Stack.Screen .../>
<Stack.Screen .../>
</Stack.Navigator>
</MessagesProvider>
that it would be much better. As you can guess we split MessagesProvider but we didn’t put it to entry point. Because i18n and Member providers are needed for general access but Messages will gonna use only for message scope and it will trigger update only that part. So we can expect the message context to update the message section, right?
Conclusion
Well, I tried to explain some the lifeblood issues of React a little in my own way. I hope it was a good and helpful article for you readers.
As like I said on top, React is really amazing library for creating this kind of architectures. When you wish to work clean, it offers you opportunities as much as it can. You can create useful and good performance web/mobile application with quality codebase.
If you have any feedbacks I would love to hear them.
See you soon on next article, be careful and stay safe! ✌
“Clean code always looks like it was written by someone who cares”
― Michael Feathers
Posted on October 4, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
May 26, 2024