This is a test task for the Senior Software Engineer job. The goal of this task is to implement a simple mail client using React
and RxJS
as a state manager.
React
RxJS
Bootstrap
andBootstrap Icons
- For simplicity of the UI and quick development
React Router
TypeScript
Vite
Deployed with Netlify
- Clone the repository
- Run
npm install
- Run
npm run dev
- 3 column layout
- Folders
- Emails list
- Email preview
- Features and Behavior
- Emails are sorted by date and grouped by folder
- Preview email by clicking on the email in the list
- Used sanitize-html to sanitize the html
- Mark email as read or unread
- Delete email
- Simulate new email
- Added to any of the folders randomly
- Email has context menu to mark as read or unread and delete
- Empty state for the emails list of the folder
- Empty state for the email (after the email is deleted or url has the id of the missing email)
- Keyboard navigation
- Arrow keys to navigate between emails
- Enter to open email preview and focus on the email preview
- Tabbing between emails and folders
- Tabbing in email context menu
- Escape to close email preview
- Data storage
- Mock data is stored in the
src/lib/services/mocks/emails.ts
file - Simulate network request and delay
- Mock data is stored in the
I decided to bind the app UI state to the router. This is a common approach which sets user in center by allowing for example to copy the url and get back to the same state when needed. Other email clients like Gmail
or Outlook
also use this approach.
It wasn't a pleasant experience, I had issues with Suspense
, proper routing, route fallbacks and other edge cases
I was picking between 3 approaches to manage RxJS state:
I am usually skeptical about any black box solutions which have a steep learning curve so I firstly tried to write my simple custom hooks to manage the state of the observables. After couple of the first tries I failed - commit. And then started to come back eventually - commit.
Then I decided to use the react-rxjs approach. It was the most straightforward and easy to understand in the beginning. Might not be the best approach in the long run, but given the time constraints and limited experience with RxJS
in combination with React
it was the best choice for me.
There are examples of the jet-blaze Todo app and React-RxJS Todo app which I used as a reference. Given my low familiarity with RxJS
+ React
+ signals
together, I decided to go with the simplest custom architecture to start with.
I used an approach which is suggested by the jet-blaze and ended up with the following custom architecture:
├── Header
├── Header.controller.tsx
├── Header.view.tsx
└── index.ts
The view
file is a pure component which is responsible for the UI. The controller
file is responsible for the business logic and the state management.
This is the simplest example of the architecture which decouples the UI from the business logic and the state management.
Example of the EmailsList.view.tsx
component:
export type EmailsListViewProps = {
// ..
};
const EmailsListView = (props: EmailsListViewProps) => {
const { emails, onReadOrUnread, onDelete } = props;
// UI part ...
};
export const EmailsList = memo(
connectController(useEmailsListController, EmailsListView)
);
Example of the EmailsList.controller.tsx
file:
const [useEmails] = bind((folder: string) =>
EmailsService.getEmailsByFolder(folder)
);
export const useEmailsListController = (): EmailsListViewProps => {
const { folderSlug: folderSlugParam } = useParams();
const folder = folderSlugParam ?? DEFAULT_FOLDER_SLUG;
const emails = useEmails(folder);
// other hooks calls ...
if (!folderSlugParam) {
return {
emails: [],
onReadOrUnread,
onDelete,
};
}
return {
emails,
onReadOrUnread,
onDelete,
};
};
Simple components like Dropdown
are preferred to be stateless and have no controller.
Services are responsible for the data fetching and manipulation. The structure is following:
src/lib/services
└── emails.service.ts
In future it's better to give the services less responsibility and split them to data manipulation, data fetching and data caching.
Initially I had an issue of using the BehavioralSubject
for the UI state. It wasn't obvious if it's OK to handle the UI and the requests observables separately. Even though it sounds reasonable and resembles the default request -> setState approach, I doubted it and was looking for something else without building a whole bunch of custom observables.
It appears that it's OK to handle the UI and the requests observables separately and eventually combine them in the components. This is how it's done and recommended by the libraries like react-rxjs
and jet-blaze
.
Eventual approach:
- Used
delay
operator to simulate the network request. - Used
useObservableAction
hook to manage the actions and their loading states. - Hold a separate
BehaviorSubject
for the UI state and subscribe to the backend requests manually