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.
- The project folder structure
- Basics of Functional-Components
- Handling State Management
- Misc Code Styling Rules
- 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
- 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 typeTMakeOptional
for make certain properties on an object optional). - utils/ miscellanous shared logic. Could be modules or inventory scripts.
- constants/ shared miscellaneous constant variables (i.e.
- 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 likehttps://my-site.com/home
,https://my-site.com/account
,https://my-site.com/posts/edit
, andhttps://my-site.com/posts/new
thepages/
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 theid
of aPost
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).
- NOTE: You should try to structure your
- styles/ various shared styles (i.e. Colors.ts)
- 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 additionalcomponents/
folder along sidecommon/
. However, DO NOT placecomponents/
(or any other shared folders) insidecommon/
, we do not wantcommon/
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 appropriatecommon/
,support/
folder etc (see Typescript best practices) and create new folders for child components which have their own children.
- 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
- 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 theTypes
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 theTypes
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.
- Building off of Typescript best practices, create a new region for functional-components called
Components
and place it betweenRun
andFunctions
, 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 largereturn
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).
- 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
/>
);
}
- Extract the component properties at the top of the function-component from the
props
param, don't useprops
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).
// 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;
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.
- 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 themsetState()
. The other functionresetState
is also useful for things like modals where you need to reset the state when the modal closes.
- 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
orredux
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, usecontext
or a global state manager likeredux
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 usescreateContext()
whose default export is the context's provider (see 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 multipleuseContext
providers and only use each at the highest level it needs to be.
(This for the most part does not include things covered by the linter but there could be some overlap)
- 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>
)}
- 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.
// 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,
},
},
};
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>
);
}
- 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>
);
}
- 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.