Prolog App
Tools
- nextJS
- react
- typescript
- react-query
- SCSS
- cypress
- storybook
Introduction
Towards the end of 2023, I felt comfortable building complex single-page applications with React and just began getting acquainted with frameworks such as Next JS. However, I did not have much experience working with professional software development cycle project or team. After a bit of research, I found the React Job Simulator course, which promised to provide real-world React development experience while expanding my skillset along the way. I took the gamble on the course, and months later, my React skills feel more refined and my problem-solving skills when working on a project of a larger scale feel improved as well.
The application being worked on is a Sentry-like service where users can view and filter through issues on deployed projects. At the beginning of the course the application is functional, but is missing some useful features and contains various bugs. The goal at the end of course was to fix these issues and implement features in a timely manner through the agile software development method of Kanban. Tasks of various difficulty were ready to be assigned to myself (the software-engineer) and increase in difficulty as they are completed. There was no recommended set of tasks to complete in a specific order; the idea is to determine what could be completed in a reasonable amount of time and how much time would more difficult tasks would take.
This page will discuss the tooling of the application, the development process, and three tasks of various difficutly that I solved during my time with the course.
Techstack Used
The section will dicuss the general tooling used for the application. The course was focused more on front-end development, so the backend was already deployed with documentation provided for API troubleshooting and information.
Next JS
The application is built with Next JS, a React framework for building a full-stack web applications. This application is developed with version 13
of the framework, but opts into the /pages
directory feature. The framework gives us the the benefit static page genreation while developing with React.
All pages can be found within the /pages
directory, in this case, each page acts as a container for a feature
component that will be discussed later. The project is configured with TypeScript, giving us statically safe-typing and allowing us to ensure that components properly recieve the defined args to avoid syntax errors.
Storybook
Component driven development is a software development method that puts emphasis on creating loosely coupled and modular components. A tool that I believe works in conjunction with this development philosophy is storybook, a library for building UI component in isolation. I had never used Storybook
, but I understood that it allowed us to easiy develop components without having to insert them into the application directly. It provides documentation for every component created, allows us to create different versions of the component, and customize the component options with options.
You'll see in the tasks section how storybook is used to create re-usable components that keep consistent style through the application.
Cypress
Cypress is a JavaScript testing framework for both end-to-end test and unit test. This application only opts into end-to-end test. Along with other scripts, an attempted merge into the repository main
branch will run Cypress test to ensure that error-riddled pull-request do not enter our code-base. Certain tasks throughout the course would not be accepted until Cypress test for the implementation were created and the behavior was validated.
Other Tools
Other tools used during the development of the applicaiton include:
- ESLint, lint the codebase and catch various issues based on the
.eslintrc
file. - Sass/SCSS, language extension for writing CSS, this application implements SCSS modules.
- Stylelint, CSS linter to force conventions defined in the
.stylelintrc.js
config file. - husky, run scripts on the codebase as the commit leaves the staging area.
- lint-staged, used in conjunction with
Husky
, provides a more efficient config for linting code in a commit. - Figma, a collaborative user interface tool to create the design of the application.
Development
This section will discuss three tasks that were assigned during the course, each with a varying level of difficulty. Each tasks deal with various aspects of the application, ranging from simple bug fixes to critical feature implementation. There were about 20 tasks total in the course.
Page Info has the Wrong Color
Expected behavior: The text color of page
info on the bottom of the issue list on the /issues
page should match the design.
The current color on the page
info blends into the light background, making it difficult to read for certain users and does not meet contrast standard set by WCAG (SC 1.4.3)].
Investigation
This one was one of the first task completed, I had a general location on where the issue was, but I wanted to take my time to understand the project structure. Taking the time to get acclimated with the configuration and already existing features would make future tasks less time consuming and easier to understand.
This project was strucutured with the bulletproof-react method, which is a opionionated method to organize a production-ready React application. It provides best practices for the API layer, testing, performance, and more.
After taking the time to feel comfortable with the development environment and locating the problematic file, it was time to implement a fix.
Implementation
The issue can be found in line 63 of the `issue-list.module.scss file. The color did not match the value defined in Figma, all colors in this project are extracted from the color.scss file. After adjusting the variable name, I confirmed myself that the color was right and met the WCAG standard.
Task Conclusion
Overall this was a good task to get started with in the course. I was able to get acclimated with the project structure and feel comfortable navigating the code-base. The fix itself was simple, just change a color variable, but it was an important fix since it resolved a critical accessbility issue.
Create Button
Requirement: Create a Button
component that could be used throughout the application to provide consistent style and functionality. The component should render in small, medium, large, and extra large sizes and should have the following states/color:
- primary
- secondary
- gray
- empty
- empty gray
- error
- disabled
A version of the button that renders an icon must be created as well.
Investigation
When I first started the tasks I didn't believe it was going to give me too much trouble, all I had to do was just provide some props to a button and it will render based on those props! That idea was right, but the issues came from implementing this in Storybook
. I had never worked with storybook before, so determining how to properly setup the component was a challenge. Luckily I had a reference to work with in the Badge.tsx component, but I felt that implementation was lacking.
I decided to take the time to read through the documentation for Storybook, not just focusing on the React
aspect of it, but general best practices as well. One of the most interesting topics I found was that the tool provided documentation for components created. After some reading, I decdided that instead of creating one component that has various options, it would be best to create different versions of the component which the documentation can display for developers.
Implementation
Let us discuss the Button.tsx component itself first. Just like Badge.tsx
, multiple enum
constants were created that defined the type name and value. These enums were set as:
ButtonColor
ButtonSize
ButtonIcon
These enum
sets would be used to define the props for the button component. However, these props themselves would not be enough to create a true button component. The component should not just look like a button, it should provide the same behavior as an HTML button
as well. The Button.tsx
component has its props set to the defined enums
and all the HTML attributes of the button
element. See the line below:
export function Button(
props: ButtonHTMLAttributes<HTMLButtonElement> & ButtonProps
);
Using a intersction type &
, we can say that our button takes in our ButtonProps
and all HTML button
attributes. To apply both our props the the attributes, the following JSX was implemented:
const { color, size, icon, ...buttonProps } = props;
const buttonColor = color === undefined ? ButtonColor.primary : color;
const buttonSize = size === undefined ? ButtonSize.md : size;
const buttonIcon = icon === undefined ? ButtonIcon.none : icon;
return (
<button
className={classNames(
styles.button,
styles[buttonColor],
styles[buttonSize],
styles[buttonIcon]
)}
{...buttonProps}
/>
);
Variables are created to extract the props for the button, with the ternary operator used to provide a default value if the prop is not passed in. The props are then spread into the button
element that is returned by the component. The Button
component can now be used throughout the application and tested properly with Storybook
as well.
Storybook Use
In the file button.stories.tsx, the configuration for the Button
story can be found with the following snippet:
const Template: StoryFn<typeof Button> = ({ size, color, icon, children }) => (
<div style={{ padding: 50 }}>
<Button color={color} size={size} icon={icon}>
{children}
</Button>
</div>
);
export const Primary = Template.bind({});
Primary.args = {
size: ButtonSize.sm,
color: ButtonColor.primary,
children: 'Button CTA',
};
Template
defines a component to use as a container in Storybook
, and the Primary
component is a story that will render the Primary
state of the button with the args provided. The file contains more stories that represent the states and styles of the component.
Application Use
With the Button
component completed, any instance of button
in the application should be replaced by our custom Button
component. See the ContactModal.tsx component snippet:
<div id="modalButtons" className={style.buttons}>
<Button color={ButtonColor.gray} onClick={() => closeModal()}>
Cancel
</Button>
<Link href="mailto:profysupport@prolog-app.com?subject=Support%20Request%20:&body=message%20goes%20here">
Open Email App
</Link>
</div>
The Button
component is used, with the color defined and the onClick
attribute set. This gives the developer a reusable component that provides consistent behavior and functionality. If more specific styling needs to be applied, the component could be wrapped within another component and targeted with SCSS modules (this is a behavior I want to try modifying in the future).
Task Conclusion
With the task completed, front-end developers working on this project now have access to a reusable Button.tsx
component that can be implemented for any instance of button
. Developers no longer have to worry about repeating styles or functionality, one component can take in various props and render for the use case. Having a component like this also ensures that developer implementations stay true to the design system.
The configuration made for the Button.tsx
component would continue to be used for other Storybook
components that were made during the course. Though this task took longer than I expected, I was able to learn a great deal about component-driven testing, Storybook
, and get a template to work with for future UI components.
Add Filters to Issue List
Requirement: Users wish to see a filter added to the issue list which narrows down the criteria of issues into three categories:
- State (resolved, unresolved)
- Level (error, warning, info)
- Name (project name)
Filters should persist after a page refresh and a no results page is rendered when the filters yield no results.
Investigation
This was the one of the most challenging tasks handed to me during the course, It wasn't just a simple "set a state and render data based on it", this implementation had to work with the already existing logic for fetching issues with the API.
The useGetIssues hook used to get the list of issues does not simply retrieve all issues. For optimization reason, it instead paginates the data and retrieves a set number of issues per page. Since it displays 10 issues per page, if there a total of 15 issues, page one will display 10, and page two will display the last 5. Simply iterating over the retrieved issues with the state info would not work since it will only filter for the current page, not truly retrieve a full list of filtered issues.
This required some investigation into the API, luckliy for us, the API is well-documented using the OpenAPI standard and the Swagger UI tool. Looking into the endpoint for /issues
I realized that it was possible to pass in options for the issue retrieval, these options could be used to retrieve issues based on status, level, and project name.
Knowing that the API is capable of retrieving filtered results, all that was left was to implement the UI to set some filter states, and pass that state into our useGetIssues
hook so it can properly retrieve the filtered issues.
Looking within the [issue-list.tsx component where the hook is used], the hook returns an object which contains the issue data. If we go look at the exact implementation of the hook, we can see that it uses react-query to call the getIssues async function. With this general setup of the hook in mind, we can begin to piece together how to pass in our filter options into the request.
Implementation.
I'm going to separate the implemetnation into two parts, the first one discussing the query being made to get the filtered issues, and the other to explain the UI implemented to set the filter options.
Query
Before creating the UI component itself, I wanted to at least structure how data was going to be passed into hook. To start off, the useGetIssues
hook should take in the page number and the filter options of status
, level
, and project
. See the code snippet below:
export function useGetIssues(
page: number,
status: Status,
level: Level,
search: string
);
The args are defined with the proper data types to ensure the UI implementation passes in the right values. When working with react-query, the library uses a key
to determine whether to retrieve from the cache or make a new request. When the hook is initialized, the getQueryKey
function is used to generate that key, it simply adds the filter options to an array that is returned into the useQuery
hook.
Since we inserted the new args into our hook, we also needed to insert them into the getIssues
function so it can be passed into the request itself. See the function below:
export async function getIssues(
page: number,
status: Status,
level: Level,
project: string,
options?: { signal?: AbortSignal }
) {
const params: Query = { page };
if (status) params.status = status;
if (level) params.level = level;
if (project) params.project = project;
const { data } = await axios.get<Page<Issue>>(ENDPOINT, {
params,
signal: options?.signal,
});
return data;
}
Previously it sent the request based on page, now it will insert the filter state into the parmas object if it exists in the args and return the result of the query with that param object. With the hook ready to take in the filter states, the /issue
page needed some updates to the page navigation.
In the issue-list.tsx component, the useNavigatePage function is modified to take in the target page and filter states. So all page navigation now has the ability to have the filter states pushed to the URL. In the case where a user refreshes the page, the useEffect
in the component checks the query object, extracts any valid properties, and sets their state in the context (discussed in the next section).
UI Component
As I began to plan out the UI implementation, I realized that a number of props were going to passed through various components. To me this didn't stick to DRY principles, and made the issue-list.tsx component harder to read. To better organize the feature, React context
was used to store the filter value and actions, which allowed allowed children passed in through the provider to access the state easily.
The full configuration of the context can be found in the filter-context.tsx component, but the main snippet can be seen below:
export const FilterContextProvider = ({
children,
}: FilterContextProviderProps) => {
const [status, setStatus] = useState<Status | null>(null);
const [level, setLevel] = useState<Level | null>(null);
const [projectName, setProjectName] = useState<string | null>(null);
return (
<FilterActionContext.Provider
value={{ setStatus, setLevel, setProjectName }}
>
<FilterContext.Provider value={{ status, level, projectName }}>
{children}
</FilterContext.Provider>
</FilterActionContext.Provider>
);
};
Multiple states are defined to represent the filter values, the two context
created are:
FilterActionContext
, used to hold the dispatch functions.FilterContext
, used to hold the current filter values.
By seperating our dispatch functions from the state value, we ensure that only the object containing the state values is recreated for the context on a re-render, just a general practice I picked up from Steve Kinney's React Performance course on FrontendMasters.
Instead of having to import the context and the useContext
hook into every component that needs the filter values, the useFilter
hook was created in the same file to extract the context values and return them from the hook. So now only one import and hook was needed to access the values. See the hook below:
export const useFilter = () => {
const filter = useContext(FilterContext);
const filterActions = useContext(FilterActionContext);
return { ...filter, ...filterActions };
};
With the context setup, I needed to wrap the set of components with the Provider. I decided to wrap it around the entire /dashboard/issue/
page in the issues.tsx component. Since each filter change would re-render the page anyway, we aren't unnecessarily re-rendering the entire page on a filter state change.
The Filter.tsx component holds the select
and input
elements that will handle the filter state changes. Each of these form elements contain their own state to keep track of the intermediate status of the filter value. So while a user is toggling or typing into the input, the request is not fired until a definitive change is made, or the user hits the Enter
key within the form.
Let's review one of the select
elements"
<div className={styles.filterSelect}>
<Select
options={StatusOptions}
action={(option) => {
navigateToPage(1, option as Status, level, projectName);
}}
ariaText="Filter status by 'unresolved' or 'resolved'"
groupName="issueStatusFilter"
placeholder="State"
hasEmpty={true}
value={status}
/>
</div>
Note that like Button.tsx
, a custom select
component was created for the application. It takes in various options like the HTML select
element, but since it required very custom styling, its built using a group of radio
input elemenets. (could be its own section discussing this).
Within the Select.tsx
component, when a filter value is definitively chosen and the selected option differs from the previously selected value, the action
function passed into the Select
will fire. In the snippet above, Select
hanldes the Status options and takes in the list of status values. So when action is fired, the option
arg refers to the selected Status option, and fires the page navigation. All of the navigateToPage
functions passed into the Select
components will have the page arg set to one, with every other arg dependent on the Select
component it was passed into.
An improvement I could have made lies in name state. Compared to Select
, this state lies outside the component itself, and relies on the useEffect
to update the state on a page refresh. This is a issue that has more to do with the implementation of the Input
component rather than the filter itself.
With this Filter
component in place, the application now allows users to browse through project issues based on various categories. In the event that the filter request returns no results, the NoIssues.tsx component renders a message stating no issues were found, and displays a button that resets the filter state and returns to the first page of the /issues/dashboard
route.
Task Conclusion
This was the most difficult task to complete in the course, it required me to investage the full implemetation of the /dashboard/issues
route and implement the filter feature with the already existing fetch logic. To efficiently faciliate the filter state between all components, React context was used to provide the state data into the children components, with the useFilter
hook used to extract the data from the context in a more organized manner.
When working on tasks, I try to implement as many best practices as possible while trying to complete the it in a reasonable time-frame. In my opinion I more time on this tasks that I expected, but I believe the experiece I gained will have a positive impact on future applications I work on, whehter it be personal or in the workforce.
Conclusion
Before starting the React Job Simulator Course, I had experience with React but didn't know how to apply it in a professional setting. My month and a half with the course gave me valuable experience that improved my React skills, refined my software troubleshooting skills, and gave me a better understanding on how to properly plan out and implement software features in a project of larger scale.
Though I've completed almost all the tasks that were provided, I plan to return to the application to make some changes I believe could improve the product. Thank you Profy.dev
for creating the course and giving me valuable experience. I expect these skils to translate well into my first software engineering job!
If you see any areas of improvements to the application that could be made, please feel free to open a pull request!