Skip to content

Instantly share code, notes, and snippets.

@jcarroll2007
Last active September 30, 2020 18:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jcarroll2007/d12acbfe1e22d354776f10d10184a131 to your computer and use it in GitHub Desktop.
Save jcarroll2007/d12acbfe1e22d354776f10d10184a131 to your computer and use it in GitHub Desktop.
React App Structure

React App Structure Proposal

Directory Structure Best Practices

If you've ever read a tutorial for client side web deevelopment, it's likely that you've come across the "beginners" directory structure that is structured by functionality and then feature.

In React that might look like

App/src/
  components/
    - Button/
    - UserAvatar/
    - ...
  containers/
    - LoginForm/
    - UsersList/
  utils/
   ... and so on

Our Applications source is split up by functionality components, containers, utils, etc. (It's ok if you aren't familiar with containers yet, it's just used for the sake of exmaple.) I called this the "beginners" directory structure because as an application scales, this structure doesn't generally scale with it. Imagine FullStory being structured like this. We have hundreds of different components maintained by many different teams. Naming, finding components, code owning would all be a nightmare!

So, we created the paradigm of {domain}-ui packages. Inside of our packages directory you will find many different packages with this naming technique. They are all different verticals of our application that are generally owned by one team. For example the dashboards vertical of the app has its own dashboards-ui package. This package contains all of the UI code necessary to render the dashboard feature of our application (or at least it should - currently there are still some dependencies which prevent this, but we're working towards this goal).

Separating out our UI code by feature, allows us to solve the problems listed above (naming, finding components, code owning). So, as our app continues to scale, we should break out logical chunks into UI packages that contain all of the code necessary to render a specific feature of the application. Sometimes we might not get those features/domains separated correctly right away - that's why it's always ok to move code around and create new packages.

But, by colocating code based on feature, it allows us to then use the "beginners" directory structure defined above as the structure for our specific feature quite well - but we can take that even further. To dive into that, continue reading.

Example Dir Structure

Below is a high level view of what a directory structure might look like for a specific vertical/feature/domain of a React application called admin-ui.

admin-ui/
  ---components/
     ---UsersList/
        ---index.tsx
        ---index.story.tsx
        ---index.module.scss
     ---index.ts
     
  ---containers/ 
     ---UsersList/
        ---Component.tsx
        ---index.tsx
        ---Skeleton.tsx
     ---index.ts
     
  ---views/ 
     ---Root/
        ---index.tsx
     ---Home/
        ---index.tsx
     ---Users/
     ---UsersCreate/
     ---UsersEdit/
     ---index.ts
     
  ---domains/
    ---orgs/ 
       ---components/
          ...
       ---containers/
          ...
       ---views/
          ---
     
  ---hooks/
     ---useResizeObserver.ts
     
  ---theme/
     ---Provider.tsx
     ---index.ts
  
  ---routes/
     ---index.ts
     
  ---contexts/
     ---Grid/
        ---index.tsx
     ---index.ts
     
  ---utils/
     ---formatDate.ts
     ---index.ts
     
  ---api/

Important Note: It might make more sense to call the components directory presentational-components as to avoid the confusion that there is only one directory that React components live in. This is not true. views, containers, and components are all React components at their most basic level, but they are components that serve different primary purposes.

views/

Let's start with views, since they are the highest order component in our app structure. A view is a very lightweight React component that primarily concern itself with defining the routing structure of its own sub-domain and the layout of it's nested containers/components.

View Traits

  • is composed of components and/or containers
  • matches a route or set of routes
  • may contain nested views (which map to nested routes)
  • should not fetch any data
  • should not contain and custom styles

views/ Directory Traits

Prefer Flat over Nested

It is generally recommended that the directory structure for views is flat over nested.

Example

views/
   ---Users/
      ---index.tsx
   ---UsersCreate/
      ---index.tsx

is preferable to

components/
   ---Users/
      ---Create.tsx
      ---index.tsx

For more info on flat vs nested directory structures see

One view per file

It is generally recommended to have one view per file.

Pascal cased directories

View directories should be PascalCased. (e.g. views/Users/) All files that contain a component should be PascalCased as well. If there are nested components, then those files should also be pascal cased. (E.G. views/Users/Create.tsx) except in the case of index files. (E.G. views/Users/index.tsx is proper casing).

Naming convention {Domain}View

It is recommended that View component names be suffixed with View (e.g. UsersView or SettingsView). This can help increase clarity in debugging in devtools and with error messages.

Example

Admin/views/Users/index.tsx

import { React } from 'react'
import { Switch, Route } from 'react-router-dom'
import { UsersListContainer } from '../containers/UsersList'
import { UserCreateView } from '../UserCreate'
import { UserDetailView } from '../UserDetail'

export const UsersView: React.FN = () => (
  <Switch>
    <Route path="/users" component={UsersListContainer} />
    <Route path="/users/create" exact component={UserCreateView} />
    <Route path="/users/:id" exact component={UserDetailView} />
  </Switch>
)

Admin/views/Root/index.tsx It's common to have higher level views that create a layout and render other views in them. A root view, for example, might use some layout components to define a top and left bars for other UI content. They themselves might even be views if they are contextual based on route. This is just an example, though, it is also common to delegate layout of main components to lower level views if there is a need to have custom layouts in those views (like no top bar or side bar).

import { React } from 'react'
import { Switch, Route } from 'react-router-dom'
import { Layout, Left, Top, Main  } from 'layout-components' // made up library of layout components
import { AppBarContainer } from '../containers/AppBarContainer'
import { NavMenuView } from '../NavMenuView'
import { UsersView } from '../UsersView'
import { SettingsView } from '../SettingsView'

export const RootView: React.FN = () => (
  <Layout>
    <Top>
      // Here in our Top component we render a container, but in the Left component we render a view.
      // This is just to show that different types of components are used here and each one has its own
      // concerns. From looking at this at a high level, we can understand that the AppBarContainer 
      // is most likely not contextual to the routing structure, but the NavMenuView is.
      <AppBarContainer />
    </Top>
    <Left>
      <NavMenuView />
    </Left>
    <Main>
	  <Switch>
	    // Notice that the routes are not `exact`. This delegates sub routes to each respective view.
	    // The UsersView and SettingsView are responsible for all routing in their own domains.
	    <Route path="/users" component={UsersView} />
	    <Route path="/settings" component={SettingsView} />
	  </Switch>
    </Main>
  </Layout>
)

Soapbox: The example above is really powerful because of clear separation of concerns. If an app consistently defines these three categories of components, it's very clear where one might start when adding a new feature or debugging. Need to update or add new routes? Find the right view. Need to change layout of the app at a high level? Find the right view. Need to fix a bug with how your TopBar is rendered? UI is created by components, so debug the Top component in this example. Need to fix a bug with data fetching? Find the right container.

containers/

A container is a React component which handles asynchronous actions, primarily data fetching and code splitting. Containers are great logical places to split code and lazy load dependencies and for fetching data and handling the rendering of asynchronous UI.

Note: From above "...and handle the rendering of asynchronous UI": it should be noted that containers do not define UI styles (CSS, or UI related), but may render different components based on different asynchronous states.

container traits

  • may fetch data or retrieve it from the data store
  • should (generally) asynchronously import itself with React.Lazy and display a fallback ui when loading the component (i.e. code splitting)
  • should not define any ui styles (css or directly applied html styles)
  • is a composition of components
  • can contain nested containers but proceed with caution*
  • should not contain nested views
  • should handle asynchronous UI states (during data fetching and code splitting)

* Caution should be used when nesting containers because nested containers create trees asynchronous dependencies. In other words - if a ChildContainer is nested in ParentContainer, then ParentContainer must resolve before fetching ChildContainer which may also have its own independent loading state. This can create micro loading states and potentially a poor experience for users. For this reason, it is generally suggested that containers are not deeply nested and as flat as possible. (TODO: add more detailed info on this)

components/ Directory Traits

May have a respective component with a similar directory name

In the example below, the UsersListContainer also maps data to the UsersList component and the directories they are both defined in are named the same. This is ideal and can create a clear connection between the two components yet distinctly separate their concerns.

Container Makeup

A container is made up of three primary files. (See the code example for more details on this)

containers/
   ---UsersList/
      ---index.tsx
      ---Skeleton.tsx
      ---Component.tsx

index.tsx is the primary file that imports the component asynchronously and handles displaying the Skeleton component during asynchronous states. The coding example can help explain this a little bit better

Skeleton.tsx this file contains a component that defines the loading state for the container that is displayed during asynchronous actions. This may either just be a loading spinner or a high fidelity component that looks similar to the fully loaded state of the container. In the example of a UsersList component the UsersList component itself should ideally define a high fidelity loading state that can be used by the container - but this is a little bit outside of the scope of this document.

Component.tsx This file should export a single component as the default export that will be imported via React.Lazy in index.tsx and should handle data fetching and mapping that data to the respective UI components.

Example

An implementation of a UsersListContainer with the following dir structure.

containers/
   ---UsersList/
      ---index.tsx
      ---Skeleton.tsx
      ---Component.tsx

Admin/containers/UsersList/index.tsx

import { React, lazy } from 'react'
import { UsersList } from '../../components/UsersList
import { UsersListItem } from '../UsersListItem'
import { UsersListContainerSkeleton } from './Skeleton'

const UsersListContainerComponent = lazy(() => import('./Component')

export const UserListContainer: React.FN = () => {
    // The implementation of this hook is abstracted because your method of data fetching 
    // is outside of the scope of this document.
	const { data, error, loading } = useUsersList()

    if (error) return error.message // Naive error UI implementation - there are better approaches here.
	if (loading) return <UsersListContainerSkeleton />    

    return <UsersList users={data.users} />
}

components/

A component, maybe more appropriately called a presentational component, is a react component that renders a UI based on state/props.

component traits

  • may define it's own styles / is a presentational component
  • may (and ideally) has storybook stories
  • generally described as "molecules" that are composed of "atoms" (atoms being components from the core component library)
  • does not necessarily have to be reused in multiple places to be considered a presentational component
  • does not connect to data stores or fetch its own data
  • should not be class based and only functional components (TODO: Explain this more in a different section)
  • A component should ideally define it's own loading state (TODO: Add a section on this)

components/ Directory Traits

Prefer Flat over Nested

There may be situations in which it makes sense to create files other than index.tsx, index.stories.tsx or styles.module.scss. This should only be necessary if the new file contains code that is ONLY used by the component which it is nested in. If it is used by other components, it should live in a more accessible location and not inside of a specific component directory, probably. However, on occasion, you might want to create components or write other code that is in a separate file that is specific to the component you are building. If this is the case, you can create a directory structure that suits your needs. However, there are two suggested ways we have approached this in App Frameworks.

  1. Flat nested file structure. In our component library, the SelectMenu component is broken into two main components - but they are internal implementation detail and therefore not both exposed to the consumer of the component. But, they are both complex enough to warrant having them in separate files. The structure for the directory looks like the following:
components/
    ---SelectMenu/
       ---index.tsx
       ---index.stories.tsx
       ---styles.module.scss
       ---Menu.tsx

Menu.tsx is where the internal component lives. This component is quite uses styles define inside of styles.module.scss and is imported and used only in index.tsx. However, let's say we want to have styles for this component in a separate file and maybe tests too. If that is the case, you could either create files namepsaced to Menu (e.g. Menu.styles.scss) or proceed to option 2.

  1. Nested directories Continuing on with the example of the SelectMenu above, let's say that there are more than one internal components, or that there are other files that contain internal code for this component. You can create other nested directories for those files like so.
components/
    ---SelectMenu/
       ---Menu/
          ---index.tsx
          ---index.styles.scss
       ---Button/
          ---index.tsx
          ---index.styles.scss
       ---hooks/
          ---useSorting.ts
       ---index.tsx
       ---index.stories.tsx
       ---styles.module.scss

Reminder: If a component is exposed to other components or used elsewhere, it is recommended that it is not nested inside of another component's directory. Flat directory structures help keep our code base consistent and readable.

One component per file

It is generally advised to have one component per file.

Pascal cased directories

Component directories should be PascalCased. (e.g. components/UserList) All files that contain a component should be PascalCased as well. If there are nested components, then those files should also be pascal cased. (E.G. components/UsersList/UsersListItem.tsx) except in the case of index files. (E.G. components/UsersList/index.tsx is proper casing).

Example

Admin/components/UsersList/index.tsx

import { React } from 'react'
import { UsersListItem } from '../UsersListItem'

export const UserList: React.FN = ({
  users
}) => (
  <ul>
    {users.map((user) => <UserListItem user={user} />)}
  </ul>
)

domains/

Domains is an optional directory that is a little bit different than the other types of directories. On occasion, you might find that your package contains multiple sub-domains. An example in FullStory is our settings-ui package is currerntly one package. However, it is owned by multiple teams. We anticipate that if domains didn't exist, that components, containers, views, etc. would begin to suffer from the same issues described in the Directory Structure Best Practices section. We might begin to have many unrelated components all in the same monolithic directory. However, it might also not make sense for these sub-domains to be their own packages. So, this is where the domains directory can help.

The domains directory can consist of more directories that are sub-domains to the specific vertical they are defined in and that should have the same directory structure defined in this document.

Let's look at an example:

--- settings/
   --- components/
       ---SettingsCard/
   --- domains/
       ---recording/
          ---components/
             ---RecordingSettingsCard/
          ---containers/
             ---RecordingSettings/
          ---views/
             ...etc

Continuing on with the example of our settings vertical in the FullStory app, adding a new domain for the Recording section allows one team to code-own their specific sub domain within the settings domain and gives them a directory/namespace to have freedom to name their own components, containers, views without colliding with other teams in the settings super-domain.

Terms

  • Presentational component: "UI is a function of state". Presentational components embody this quote perfectly as their sole purpose is to render some UI based on their props/state.
<Switch>
<Route path="/users" exact component={UsersView} />
<Route path="/home" exact component={HomeView} />
<Route path="/settings" exact component={SettingsView} />
</Switch>
<Switch>
<Route path="/users" component={UsersList} />
<Route path="/users/create" exact component={CreateUserView} />
<Route path="/users/:id" exact component={EditUserView} />
</Switch>
@jcarroll2007
Copy link
Author

MacBook Pro - 1 (1)

@jcarroll2007
Copy link
Author

  • add high level less detailed doc that links to respective pages for components views etc
  • add an outline at the top

@jcarroll2007
Copy link
Author

jcarroll2007 commented May 18, 2020

  • add naming conventions for views ({ViewName}View) and containers and components

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment