Skip to content

seanpmaxwell/React-Ts-Best-Practices

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

72 Commits
 
 

Repository files navigation

React-Ts-Best-Practices

Documentation for best practices to use with React with Typescript. Note that this documentation builds off of Typescript best practices, so we won't mention practices that are already mentioned there.

Table of contents


The project folder structure

Project Folders Overview

- app
-   public/
-   src/
-     assets/
-     common/
-     components/
-     models/
-     pages/
-     styles/
-     App.test.tsx
-     App.ctx.tsx
-     App.tsx
-     index.css
-     index.tsx
-     react-app-env.d.ts
-     reportWebVitals.ts
-   .env
-   .eslintrc.json
-   .gitignore
-   README.md
-   package.json
-   tsconfig.json

Folders Explained (files generated by create-react-app are not listed)

  • public/ generated by create-react-app (holds assets like favicons)
  • src/ generated by react
    • assets/ downloaded assets like images, third-party fonts etc
    • common/
      • constants/ shared miscellaneous constant variables (i.e. Paths.ts which holds all the route string values of your APIs).
      • types/ shared miscellaneous types (i.e. types/index.ts which has the custom utility type TMakeOptional for make certain properties on an object optional).
      • utils/ miscellanous shared logic. Could be modules or inventory scripts.
    • components/ shared JSX components (i.e. a styled button you use in multiple places)
      • lg/ single components that take up multiple files
      • md/ single components that take up one file
      • sm/ multiple components per file
    • hooks/ any custom hooks you create
    • models/ modules which represent data-tables (i.e. User.ts represents the users table in the database)
    • pages/ the various pages of your application.
      • NOTE: You should try to structure your pages/ folders as close as possible in the same way as they are navigated to by the user. So if your site is like https://my-site.com/home, https://my-site.com/account, https://my-site.com/posts/edit, and https://my-site.com/posts/new the pages/ folder should look like how it does in Snippet 1. Of course this is not always possible and it's normal to not follow this to-a-tee. For example, you might have the id of a Post record inserted somewhere in your url, so it can be automatically selected when the user refreshes the browser and 'View' might be the default view for a particular post that's selected (i.e. https://my-site.com/posts/9Z8AO5C844R displays the component).
    • styles/ various shared styles (i.e. Colors.ts)

Structuring your app

  • In addition to what's listed above, you should name your files/folders after the React component they mean to represent. If a file represents a single-component, and there are not multiple files for that component (that includes test files and child components), name that file after the component. However, for large components that include multiple files, name the folder after the component, and name the core file index.ts, which is the JavaScript convention for naming the file in a folder where everything else is exported from (see Snippet 1).
  • Please see Typescript best practices for the reasoning behind creating the common/ folder and its three subfolders. For React applications, because we may have shared JSX components across multiple parent components, we create an additional components/ folder along side common/. However, DO NOT place components/ (or any other shared folders) inside common/, we do not want common/ to be a dumping ground.
  • For folders which represent a single component, all files directly in that folder should represent child-components of the default component in index.tsx file. Place non-jsx content it's appropriate common/, support/ folder etc (see Typescript best practices) and create new folders for child components which have their own children.

Snippet 1

- components/
- common/
-   constants/
-     Paths.ts
-   types/
-   utils/
- hooks/
- models/
-   User.ts
-   Post.ts
- pages/
-   Home/ (https://my-site.com/home)
-     index.tsx
-     index.test.tsx
-   Account/ (https://my-site.com/account)
-     UpdatePaymentForm/
-       ValidatePaymentInfo.tsx
-       index.tsx
-       index.test.tsx
-     index.tsx // <- Imports the <UpdatePaymentForm/> component
-     index.test.tsx
-   Posts/ (https://my-site.com/posts)
-     common/
-       types/
-         index.ts // Has stuff that's used by both the View, Edit, and New pages
-     components/
-       PostForm.tsx // Used by both New and Edit pages
-     Edit/ (https://my-site.com/posts/9Z8AO5C844R/edit)
-       index.test.tsx
-       index.tsx
-     New/ (https://my-site.com/posts/new)
-       index.test.tsx
-       index.tsx
-     View/ (https://my-site.com/posts/9Z8AO5C844R)
-       index.test.tsx
-       index.tsx // Displays a particular post
-     index.tsx // Displays the <PostsTable/> component if no singular post is selected.
-     index.ctx.tsx
-     index.test.tsx
-     PostsTable.tsx
- styles/
-  Colors.ts
-  BoxStyles.ts

Basics of Functional-Components

Declaring

  • Use PascalCase for naming functional-components.
  • Use functions for declaring components not classes. Procedural/functional programming is the dominant trend in JavaScript and is much more convenient for making smaller components. Use function declarations (function) not arrow functions for making components so that they are hoisted.
  • For components declared in the same file, follow a top down approach. That is, declare children components below the parent so that the pattern of programming for both for doing logic and creating elements stays consistent.
  • If you use TypeScript, always typesafe a functional-component's properties. For large/complex components, create an interface for the props argument (i.e. IProps) and place it in the Types region of the file (see Typescript best practices). If you're using JSDoc, you should also typesafe the properties for shared components, but for single using components (like a the file for a page) specifying the types might be overkill. If a jsdoc custom type also gets large/complex, you could also place it in the Types region of your file. Both for typescript and jsdoc, you don't need to specify the return type for functional components cause its always JSX.Element. A good way to remember these rules is that whenever another developer needs to use the component you created, your typesafety should reflect that.

Organizing the code of a functional-component

  • Building off of Typescript best practices, create a new region for functional-components called Components and place it between Run and Functions, see (see Snippet 3).
  • Don't place static values inside functional-components, if you do they'll have to be reinitialized everytime the component state is updated, which could affect performance for large complex applications. Place them at the top of the file under the Constants region (see Typescript best practices).
  • If a function inside a functional-component is large and its logic does not need to change with the component, move it outside the component and put it in the Functions region at the bottom of the file: this is will stop the logic from needing to be reinitialized each time.
  • When positioning sibling-components in relation to each other, do the positioning in the parent-component, that way all the positioning between siblings can be seen at once and we don't have to dig into the code of each individual child component to move them (see Snippet 2).
  • Although comments (not spaces) should generally be used to separate chunks of logic within traditional functions (as mentioned in Typescript best practices), for jsx component-functions, we can use spacing to separate chunks of logic. Use single-spaces + comments to separate hook calls, related DOM elements within the return statement, and initializing related variables (see Snippet 3).
  • Call your hooks in the order than they are used: i.e. import direct properties at the top, then any properties from useContext then initialize your state, the place any onLoad API calls, hooks that listen for changes from DOM interaction should go in the order than the DOM elements are arranged, then any submission API calls at the very end. Like anything else it might make sense to use exceptions but this is generally how it should be organized.
  • Inside of a functional-component, don't clog the region above the return statement with a bunch of child JSX.Elements assigned to variables. Also don't create excessively large return statement for DOM elements. Create new child JSX.Elements and group together chunks of related DOM code. This will make your code more readable and easier to move around if you need rearrange some DOM elements (see Snippet 2, pretend that the <Child/> elements are much larger than they actually are in the snippet).

Snippet 2

  • BAD
function Parent() {
  const posts = [];
  const name = '';

  let Child = null;
  if (something) {
    child = (
      <Box mb={2}>
        Name: {name ?? ''} Posts: {posts?.length ?? 0}
      </Box>
    );
  } else {
    Child = (
      <Box {...otherProps}>
        Foo: {name} Bar: {posts.length}
      </Box>
    );
  }

  return (
    <Box>
      <Child/>
      <SomeOtherChild/>
    </Box>
  );
}
  • GOOD
import Box, { BoxProps } from '@mui/material/Box';

interface IChildProps extends BoxProps {
  name?: string;
  posts?: string[];
}

function Parent() {
  return (
    <Box>
      {something ? (
        <Child1 mb={1} name={name} posts={post}/>
      ) : (
        <Child2 name={name} posts={post}/>
      )}
      <SomeOtherChild/>
    </Box>
  );
}

function Child1(props: IChildProps) {
  const {
    name = '',
    posts = [],
    ...otherProps
  } = props;

  return (
    <Box {...otherProps}>
      Name: {name} Posts: {posts.length}
    </Box>
  );
}

function Child2(props: IChildProps) {
  const {
    name = '',
    posts = [],
    ...otherProps
  } = props;

  return (
    <Box {...otherProps}>
      Foo: {name} Bar: {posts.length}
    </Box>
  );
}
  • In complying with TypeScript best practices, use function-declarations for functions at the top scope of a file and arrow-functions if a function is declared inside of another function:
function Parent() {
  return (
    <Child
      onClick={() => doSomething()} // GOOD
      onMouseDown={function () { ...do something else }} // BAD
    />
  );
}

Component properties

  • Extract the component properties at the top of the function-component from the props param, don't use props in a bunch of places to access values. This makes it easier to intialize default values when a property is undefined and makes the code more robust because you can see if a property is no longer being used but might still be getting passed down by parent-component. Another aspect to this is that when creating a functional-component that wraps around another functional-component, it's generally a good idea to mimick the child properties as much as you can so that way you don't have to recreate/redeclare these properties again (see Snippet 3).

Snippet 3

// LoginForm.tsx

import { useCallback } from 'react';
import axios from 'axios';

import Box, { BoxProps } from '@mui/material-ui/Box';
import Button, { ButtonProps } from '@mui/material-ui/Button';

import Indicator from 'components/md/Indicator';


/******************************************************************************
                               Components
******************************************************************************/

/**
 * Login a User
 */
function LoginForm(props: BoxProps) {
  const {
    sx,
    ...otherProps
  } = props;

  // Misc
  const navigate = useNavigate();

  // Init state
  const [ state, setState, resetState ] = useSetState({
    username: '',
    password: '',
    isLoading: false,
  });

  // Call the "submit" API
  const submit = useCallback(async () => {
    setState({ isLoading: true });
    const resp = await axios.post({
      username: state.username,
      password: state.password,
    });
    const isSuccess = _someLongComplexFn(resp);
    setState({ isLoading: false });
    if (isSuccess) {
      navigate('/account');
    }
  }, [setState, state.username, state.password, navigate]);

  // Return
  return (
    <Box
      sx={{
        position: 'relative',
        ...sx,
      }}
      {...otherProps}
    >
     {/* Indicator */}
     {state.isLoading &&
       <Indicator/>
     }

     {/* Input Fields */}
     <TextField
       type="text"
       value={state.username}
       onChange(e => setState({ username: e.currentTargetValue })}
     />
     <TextField
       type="password"
       value={state.password}
       onChange(e => setState({ password: e.currentTargetValue })}
     />
     
     {/* Action Buttons */}
      <Button
        color="error"
        onClick={() => navigate('/home')}
      >
        Cancel
      </Button>
      <Button
        color="primary"
        onClick={() => submit()}
      >
        Login
      </Button>
    </Box>
  );
}

/******************************************************************************
                               Functions
******************************************************************************/

function _someLongComplexFn(data: AxiosResponse<unknown>): boolean {
   ...do stuff
}

/******************************************************************************
                               Export default
******************************************************************************/

export default LoginForm;

Handling state management

State management is done using `useState`, `useContext`, and maybe a third-party-library like `redux`. We'll cover the best practices for all of them.

useState() and useSetState()

  • For small components that only have one or two state values, using useState directly is fine, but once a component starts to have large numbers of state values, using a custom hook to handle all the state values as a single object will make your code much more readable and easier to manage. There might be libraries for this or you copy and paste the source for the custom hook here.
  • This will make your code more readable cause now all variables that belong to the local state will began with state, and you only need one function managing them setState(). The other function resetState is also useful for things like modals where you need to reset the state when the modal closes.

useContext and "third party global state managers"

  • If a state value in a parent component only needs to go down one layer to a child component that exists in the same file, then passing it through the function properties (props) is fine; context or redux is probably overkill. If however you have a large/complex component that needs to pass data to multiple children, spread across different files, then don't use props, use context or a global state manager like redux instead.
  • For those using useContext, if your component contains both a large amount of dom content and a large amount of <Provider/> properties as well, it might be worth it break your context and your jsx code into different files. You should append these files with "file Name".provider.tsx. For example, suppose your App.tsx file contains a lof of jsx code and a lot of logic for fetching/managing the user sessions, you could create a seperate App.provider.tsx file which uses createContext() whose default export is the context's provider (see Snippet 4).

Snippet 4

  • App.provider.tsx
import { createContext } from 'react';

const AppCtx = createContext({});

function AppProvider(props) {
  const { children } = props,
    [ session, setSession ] = useState({});

  const resetSessionData = useCallback(newData => {
    const newSession = ...bunch of logic
    setSession(newSession);
  }, [setSession])

  return (
    <AppCxt.Provider value={{
      session,
      resetSessionData: val => resetSessionData(val),
    }}>
      {children}
    </AppCxt.Provider>
  );
}

export const useAppContext = useContext(AppCtx);
export default AppProvider;
  • App.tsx
import AppProvider, { useAppContext } from 'App.ctx';

function App() {
  const { session } = useAppContext(); 
  return (
    <div>
      <AppProvider>
        <NavBar>
          Hello {session.userName}
        </Navbar>
        <Home/>
        ...some large amount of jsx code
      </AppProvider>
    </div>
  );
}

export default App;
  • Keep in mind that useContext does trigger a rerender of the component using it and all child components. So make sure you create multiple useContext providers and only use each at the highest level it needs to be.

Misc Code Styling Rules

(This for the most part does not include things covered by the linter but there could be some overlap)

Conditional Elements

  • Don't need to wrap DOM elements in parenthesis for &&. Do use parenthesis for ternary-statements though:
// BAD
{isLoading && (
  <div>
    <Indicator allPage />
  </div>
)}

// GOOD
{isLoading && 
  <div>
    <Indicator allPage />
  </div>
}

// GOOD
{(isError && !!errMsg) ? (
  <div>
    {errMsg}
  </div>
) : (
  <div>
    Foo Bar
  </div>
)}

Styling the UI

  • Rules could vary depending on which library you decided to use, but generally don't hardcode hex color strings directly inside jsx elements. To keep your styling (including colors) consistent, have a src/styles/Colors.ts file which exports a single object containing all your colors (see Snippet 5). Also, in this object don't hardcode your hex strings in random places. At the top of the file, group your colors in a single object, which in turn, groups colors by their base color. Then, for the exported object, group your colors by the type of component property (i.e. Border) they are applied to.

Snippet 4

// src/styles/Colors.ts;

const Base = {
   Grey: {
     UltraLight: '#f2f2f2',
     Lighter: 'e5e5e5',
     Light: '#d3d3d3',
     Default: '#808080',
     Dark: '#a9a9a9',
     Darker: '#404040',
     UltraDark: '#0c0c0c',
   },
   Red: {
     Default: '#ff0000',
     Dark: '#8b0000',
   },
   White: {
     Default: '#ffffff',
   },
};

export default {
  Background: {
    Default: Base.Grey.Default,
    White: Base.White.Default,
    Hover: Base.Grey.Light,
  },
  Border: Base.Grey.Dark,
  Text: {
    Error: {
      Default: Base.Red.Default,
      Hover: Base.Red.Dark,
    },
  },
};

Snippet 5

import Colors from 'styles/Colors.ts';

function Foo() {
  return (
    <div>

     <div style={{
       marginBottom: 16,
       fontSize: 12,
        backgroundColor: Colors.Background.Grey.Default, // don't do '#808080'
     }}>
       Hello
     </div>

     <div
       id="how-are-you-ele"
       style={{ padding: 8 }}
       onClick={() => alert('How are you?')}
     >
       How are you?
     </div>
    </div>
  );
}

Parameter names for JSX callback functions

  • In all of progamming, function parameters should have useful names that describe their purpose, but in React, because its so common to see lots of arrow functions declared directly in JSX element properties, you can just use the name v for these simple callbacks if they only return provide one or two values. This will also help to distinguish the callback parameter from the other variables in the JSX element:
function Parent() {

  // State
  const [ state, setState ] = useSetState({
    name: '',
    nameError: false,
    email: '',
    emailError: false,
  });

  return (
    <div>
      <CustomInput
        value={state.name}
        isRequired={true}
        onChange={(v, err) => setState({ name: v, nameError: err }) }
      />
      <CustomInput
        value={state.email}
        isRequired={true}
        onChange={(v, err) => setState({ email: v, emailError: err }) }
      />
     <button disabled={state.nameError || state.emailError} onClick={'someAPI call'}>
       Submit
     </button>
    </div>
  );
}

/**
 * Custom Input that shows an error below the <input/> if value is required but not there.
 */
function CustomInput(props: { value: string, isRequired: boolean, onChange: (value: string, error?: boolean) => void }) {
  const { value, isRequired, onChange } = props;
  return (
    <div>
      <input
        type="text"
        value={value}
        onChange={v => onChange(v.trim(), isRequired && !v)}  
      />
      <div>
        {(!value && isRequired) ? 'Value is required' : ''}
      </div>
    </div>
  );
}

Other

  • If an element only has one prop being passed, you can put it on the same row as the element name, put it to the next row if there's more than one (see Snippet 5).
  • Use single quotes '' for plain JS/TS code, but use double-quotes "" for properties on jsx elements. You can set this in the linter but I'm mentioning it cause I've seen so many projects without it.

About

Documentation for best practices to use with React with Typescript

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published