We want to create a frontend app to display, send, and delete comments. A comment has this shape:
interface Comment {
id: number
name: string
created: datetime
message: string
}
As a user, I can
- Enter my
name
in a text input and mymessage
in a textarea. - Click the "Comment" button.
- See my comment appear at the top of the list of comments and my name and comment box inputs clear.
- Scroll through a list of comments posted by me or other users. Each comment has a date displayed indicating when it was posted.
- Try posting a comment while offline.
- See an error message display under the comment button and the name and comment I typed persist in the UI.
A breakdown of the sub-problems we need to solve are:
- Add a comment
- Retrieve all comments
- Display a comment
The implementation strategy will be discussed in the Implementation Overview section.
In a real comments app, the total number of comments can be huge and we don't want to retrieve all of them in a single call because that will result in network timeout.
We can implement pagination to retrieve comments in batches. However, this is not a requested feature from the PRD and looking at the server and api code, pagination is not implemented server-side.
We are not building a button that deletes all the comments. This looks like a user-facing app, not an internal tooling. We don't want any user to delete comments for other users. If we want to call /deleteComments
to purge data, we can do that in Postman.
This is an common feature for a comments app. As a user, I want to be able to click a "Delete" button next to a comment I previously posted to delete that comment.
However, this is not a requested feature from feature is not requested in the PRD and . It's not clear whether this is a feature that is not needed or it's a feature that is not needed for the MVP.
Deletion Confirmation Modal
Users can easily click the delete button by accident. We should add a confirmation modal to prevent accidental deletion because it would be a bad user experience that that the UI does not guard against irreversible actions due to user error.
However, for the sake of speed in completing this takehome project, I will not implement the confirmation modal, but I will talk about it here how I would implement it:
- Create a
ConfirmationModal
component that takes aonConfirm
callback prop and aonClose
callback prop. It also has aisVisible
prop that controls whether the modal is visible or not. - The parent component keeps the
isModalVisible
in React state. When the delete button is clicked, it changes that state totrue
. It passes aonClose
props toConfirmationModal
that changes theisModalVisible
state tofalse
. - In
ConfirmationModal
, there's a close button which triggersonClose
. TheonClose
is also called when the user clicks confirm to delete the comment and the request was successfully processed. - When the user clicks confirm to delete the comment,
onConfirm
is called. - In the parent component,
onConfirm
is used to update the component state where all the comments are kept. ConfirmationModal
is responsible for making thedeleteComment
API call and handling errors from that API call.
Before diving into development, we need to establish the client-server contract.
The API exposes a few services to the client but it's not clear from the PRD what the shapes of those responses are, but we can figure this out by looking at the server and api code and testing the API endpoints using Postman.
Here is a few ways we can generate the success and error responses:
- Use Postman to test the API endpoints by calling it with valid args to create the success response and invalid args to create error response.
- Changing the server or api code to always reject with error.
Once we have the responses for all the endpoints, we can create mock data for frontend development. API contract/mock data is a great way to enables parallelization in frontend and backend development.
When you first post a comment, it gets inserted to the top of the comments list with the label {Your Name} Just Now!
. But if you let this view sit for a while without refreshing the page, this Just Now
label should update to Today at 12:45 PM
. This requires some sort of timer to update the label every minute or so based on the new value for time now. We can do this with setInterval
or setTimeout
and pass now
as an optional parameter into getConditionalDateString
.
I thought about making a CommentBox
component that can be used to display a comment or to add a comment. It can be useful if in the future, we want to enable editing of an older comment in the list. However, I decided against it because it's pre-mature optimization. If there's a need to edit an older comment in the future, we can add that feature later on and do some refactoring.
Important for real projects, especially if we are building a consumer application. There's a blueprint for how to successfully implement a11y and i18n in React apps and I've had a lot of experience at OkCupid and Smartling working on both.
For the sake of velocity in completing this takehome project building a MVP comments app, I will not implement fancy a11y and i18n capabilities but I will create the MVP with a11y and i18n best practices so that it can easily extended to add fancy a11y and i18n capabilities later on.
There are simple things we can do now in an MVP to avoid having to refactor later on when we are more focused on a11y and i18n:
- a11y: Use semantic HTML elements. For example, use
a
for links andbutton
for buttons. Don't try to build your own links and buttons usingdiv
with onClick handlers. - i18n: Since we are using
toLocaleDateString
to format the date string, we can accept alocale
parameter and set it to English by default for now. We shouldn't spend time now testing all the other locales. Another easy thing we can do is to build reusable components that can be easily evolved for i18n (for instance, don't pass string as props. Pass children). Reusable components that are not originally built to accept arbitrary React nodes would require a huge refactor to the frontend codebase when we want to add i18n later on.
a11y
is a big topic with a lot of best practices you have to be aware of. For instance it requires a lot of extra props like aria-label
to be added to the jsx. There are tools like the a11y eslint plugin to guide you to add the right props. There are so many other tools and libraries like focus-trap-react
and react-aria that provides accessible UI primitives for your design system.
i18n
is also a big topic. Compared to a11y, there is not as many explicit guidelines for i18n standardization. There are dos and don'ts for i18n but they exist primarily in tribal knowledge form. When I worked on the internalization team at OkCupid, I created an eslint plugin to enforce i18n best practices (eslint-plugin-i18n-lingui) which we open sourced.
We are not going to support offline support for this project but if offline support were an objective, I could use this library messages-cache which I built when I was at OkCupid to supports offline-first experience for sending a message in chat.
This library is basically a class that manages a cache. It exposes various methods for updating and retrieving things from the map. The cache is a data structure consisting of a double-ended queue in a Map and supports constant time lookup by message id and constant time insertion and deletion anywhere in the queue.
For OkCupid, I integrated this data structure by putting it in a React memo and synced the cache data with messages
React state that drives the re-rendering. The messages-cache
exposes a method called syncStateWithCache
that the component using the lib can call to sync the cache with the React state.
We don't want to put the cache in the React state directly because it would cause excessive re-rendering when the dequeue is updated. The cache manager can be called to add a bunch of new messages to the end of the queue (on initial load) or beginning of the queue (load older messages). Because the dequeue recursively add nodes, when many nodes are added all at once, this would cause excessive re-rendering and crash the chat app if the cache data structure were stored in a React state.
In a real project, having TS and better linting will improve maintainability and ease of feature development as the codebase grows. But for this project, since the boilerplate starter is not already set up with TS and linting, it would require extra configuration and setup that is not worth the time for this takehome project.
We will build a custom hook usePostComment
to make the API call to post a comments. The custom hooks will do all the hard work of creating the request, mapping errors and provide a nice interface to the React component:
The custom hook will be used in the NewComment
component, which renders the input, textarea, and comment button.
We will build another custom hook useAllComments
to make the API call to retrieve all comments. This hook will used by the App
component which renders the list of Comment
, the NewComment
, and manages a React state comments
.
The Comment
component will display a comment and be passed the name
, created
, and message
props.
Comments need to be displayed in descending order based on the date created.There’s a feature I need to implement in which things need to be sorted.
I can do it in the frontend code but it’s better to do it in the backend. This is a frontend takehome but there’s some backend code is provided. Can I update the backend code for this assignment?
Let's write a function getConditionalDateString
that takes the datetime from the server and the current time and calculates a display string indicates when the comment that can be sent. The display shows "Just now!", "Today at 1:25 PM", "Yesterday at 9:52 PM", "Thursday at 3:16 PM", or "Jan 12 at 3:15 PM". This function can be written to support internationalization right off the bat, although testing it for all these different locales would be de-scoped.
We want to write a function that takes the datetime
and outputs the display string.
This function is tricky because datetime from the server is UTC and the current time, calculated from new Date()
is in the clients's local timezone.
We have to convert the datetime from the server to the client's local timezone before we do all the time math to calculate the display string.
For this project, we are implementing any sanitization of the input to prevent XSS or SQL injection, because:
-
On inspection of the server code to add a new comment, it doesn't look vulnerable to sql injection
createComment({ name, message }) { return this.dataAccessObject.run( 'INSERT INTO comments (name, message) VALUES (?, ?)', [name, message] ); }
-
The input is not rendered as html so it's not vulnerable to XSS. According to the best practice for XSS prevention, we should always try to render data through JSX and let React handle the security concerns for you.
I also double checked by entering some some sql injection strings to delete the comment table and html script to display alert into the message textarea and verified that the table is not deleted and the app is not showing an alert.
For input validation, we don't want the comment to be posted unless both name and message inputs are non-empty and contain at least one non-whitespace character. We can do this by disabling the comment button if the name or message is empty or only contains whitespace.
We also don't trim the spaces before and after the name and message before posting, which is expected for most user-generated contents on social media sites.
Date formatting
A tricky part about testing this function is that it's not a pure function as it calculates its own current time. For unit tests, we have to make sure the test passes consistently every time we run it.
We can do this by mocking the new Date()
function to return a fixed date if it's called with no argument.
UI Stress test
We can perform UI stress testing for each of our React components by checking that everything is displayed nicely when the strings are very long. We can also check that the UI is responsive when the window is resized. Verified that:
- textarea is scrollable
- comment display area scales with content
You can do these tests without making any database queries or code changes by modifying the DOM directly in Chrome Dev tools.
Responsiveness We can test responsive design by using the Chrome Dev tools to simulate different screen sizes and verify that it looks good on all screen sizes.
Unusual strings
We should also test some unusual strings like empty string, string with only spaces, string with only special characters, string with only emojis, etc.
- For our
NewComment
component, make sure we disable sending if the name is empty or just empty strings - For our
Comment
component, make sure that it renders strings with special characters correctly. For example, try this string:What's the meaning of "meaning"?
Error states
We can simulate network error in Chrome Dev tools, in the network tab, set network throttling to offline. Then, test sending a comment when the network is offline. We should see the error message.
We can simulate server error by modifying the server code or the API code to always return the error.
To simulate server outage, we can kill the server locally and test sending a comment. We should see the error message.
Perform all the actions in the user story.
Demo video uploaded to YouTube because the file is too big to upload.
- Mini Tech Design
- Create mock data for frontend development
- Build out the frontend components with support for variable size screen
- Format date based on current time with unit test
- Integrate frontend with the GET endpoints
- Integrate frontend with the POST endpoints (Spike: timezone difference)
- Handle get and post error in the frontend
- Acceptance testing and creating the demo video
- Implement form input validation