Trana Frontend

Trana Frontend


Current Setup

  • ​Part of Lerna monorepo
  • Typescript application
  • Next.js based app
  • Custom @trana/ui component library
  • Styled components
  • GraphQL over urql

Pain Points

  • ​Development server/Next.js
  • Component Duplication
  • Extremely flat file structure / hard to navigate
  • Large component filesProp drilling/Boilerplate
  • Lack of global state
  • (Un)ease of testing
  • Very permissive typing

What can we do

incrementaly and consistently

1. Development setup / Next.js

There are simply 2 options here:

 1. Depart from Next.js
       - faster devserver without crashes
       - CDN hostable frontend
       - no need to think about server-side issues
       - we're not really using Next.js killer features
       - no SSR (is that really an issue)

2. Stay with Next.js and lean into it further
    - leverage SSR for unauthorized pages
    - leverage SSR for even for authorized ( the cookie authentication should allow for it)
    - more complex code in some situations to handle hydration

2. Component duplication / Large component files / UI Lib

- Almost every component is redefining low level parts Frame/Wrapper/Button

- This is mostly a product of speed of development, and trying to match perfectly ever so slightly changing designs.

- It keeps compounding as over time it is harder to find something reusable among the large amount of components and more productive in the short term to just add another one.

- Although for people seasoned on the project it might not be so hard to know where to find things, for newcomers the flat file structure and a mixture of domain coupled and agnostic components is a hurdle

- To achieve a more organized approach with our UI, especially incrementally, I believe the Design/Product part of the company will need to be able to compromise with Development on a number of questions. We should not let perfect be the enemy of good (enough).

2. Component duplication / Large component files / UI Lib

What we can do is add more structure to our UI components

- @trana/ui/core
Domain agnostic atomic components (Button, Box(Frame))

- @trana/ui/shared or just @trana/ui
Still domain agnostic molecular components ( MultiSelectTable )

- @trana/hosting/components
Feature level organic components (CourseFeaturedCard)

For more info and examples on the 'atomic' nomenclature look here

2. Component duplication / Large component files / UI Lib

One does not simply... write a UI library
What can we do to speed things up, and not reinvent the wheel

Headless Component Libraries:

Unstyled component libs ready to use, we should pick one according to the API it provides and start styling it and using it

Good artists borrow, great artists steal!

2. Component duplication / Large component files / UI Lib

Keep the component structure in hosting organized
- src/views/  - For top level views corresponding to pages in app and code splitting points
- src/components/<feature>  - Feature level components with any subcomponents contained inside
 
src/component/CourseProgressBarStepper
├── components                                    // when non-shared subcomponents are needed
│   └── StepperButton
│       ├── StepperButton.ts
│       └── StepperButton.test.ts
├── index.ts                                            // barrell exports
├── CourseProgressBarStepper.tsx
├── CourseProgressBarStepper.stories.ts
├── CourseProgressBarStepper.test.ts

2. Component duplication / Large component files / UI Lib

A few more notes regarding the UI:

- Leverage Styled Components ability to take props

- Where are the icon designs coming from, can we define the whole set or use a package?

- We should try to leverage the UI changes to get some responsiveness

- Some attention should be paid to visual UI patterns already present, and what layouts can we extract from that, and how the same layouts behave on different screens

- The app is getting complex but we're still under 20 different screens and in the zone where we could be making progress with UI changes screen by screen at a noticable speed, gaining momentum as we progress

3. Prop drilling

Currently a top level 'Page' component prepares all necessary GraphQL queries and mutations, and any state necessary and keeps passing it down a few levels.

This leads to a lot of boilerplate and a lot of props being carried trough components that have no use for them.

React hooks were added to avoid this problem, and there is no real benefit of making a query wrapper on the top level and passing it down.
By keeping what would naturally be a child components state in the parent we're also setting ourselves up for unnecessary renders, and bad situations with recursive updates.

3. Prop drilling

The components (on the feature/organism level) should use any state hooks or global state they need by invoking it in it's own code.

This gives us much cleaner files, a better separation of concerns and will lead to better performance.

If you feel this is messing up dependency injection and will affect testability of the components, don't worry.
A simple context provider with a mock/custom value in the tests solves this issue.

4. Lack of Global State

- Lack of global state makes the app harder to debug

- Altough there is no global store, the graphql layer serves that function in the general sense with most of the apps data

- One drawback to it is that urql is not able to integrate local state OOTB

- Where local state is needed Zustand is used

- A stronger integration of urql and a state library could be achieved with something like

- Changing this is a nice to have, and would be easier/feasible after other improvements

5. Unease of testing

- Testing should become easier/feasible as we cut down the duplication in the code

- End to end tests will bring us most production safety for the investment

- Cypress is able to run unit tests also, altough we'd be fine with using jest also

- Storybook changes should be a crucial part of the UI overhaul

5. Permissive typing

To fully leverage the TS compiler as more than a glorified intelli-sense,
the configuration should be a bit stricter.

This would mean:
- not using any
- creating a core set of types  coresponding to domain Entities and keeping them updated
- adding type data on useQuery
- keeping a clear distinction between undefined and null

This will not be an easy process since the project is quite developed with the current ruleset.

- On a further note extracting the domain types into a separate lerna package might give a chance for some flexibility in the UI library of domain aware molecular components that can be typed properly

In Conclusion

A suggested tasklist to approach these issues would be:

1. Decide on Next.js and act accordingly
2. Asses general patterns in the UI with the designer
3. Have the 'hard' conversation on trying to limit new designs to a set of known elements
4. Pick headless UI lib and style it
5. Create the core of the ui library and  storybook tests
6. Approach the project Page by Page and convert/create necessary molecular/feature components to reconstruct the page
7. Keep reorganizing the component state access to organism components level and declutter the general architecture
8. Once a solid structure is in place and the application is easier to reason about reasess TS improvements and global state