Skip to main content

Best Practice

Defined here is some best practice within React that we all try to follow.

Data layout component IDs

Setting a data attr on section of the Section component can be useful in quickly identifying the components within the section via the DOM.

...

let layouts = [];

if (components?.length > 0) {
layouts = components?.map((comp) => comp.acf_fc_layout);
}

return (
<section id={id} ref={ref} className={outerClasses} key={sectionKey} data-layouts={layouts}>
...
<section>
...

Clean up of intervals and timeouts defined in useEffects

Clean up intervals and timeouts defined in useEffects. Shown below is clean up of a setInterval via a callback in a useEffect but it's the same for a setTimeout.

src/components/ClearIntervalExample.js
const ANIMATION_INTERVAL = 7;

// Change active slide every 3 seconds
useEffect(() => {
let interval;

if (isInView) {
interval = setInterval(() => {
if (activeIndex < slides.length - 1) {
setActiveIndex(activeIndex + 1);
} else {
setActiveIndex(0);
}
}, ANIMATION_INTERVAL * 1000);
}

return () => clearInterval(interval);
}, [activeIndex, isInView, slides.length]);

Use SetInterval and SetTimeout alongside isInView

As shown in the above, ensure intervals and timeouts are only set once the related component is in view.

Ensure useEffect dependencies are correct

Ensure your useEffect dependencies are correct to avoid unwanted side effects. If you're purposely defining wrong dependencies for a desired effect then your code can most likely be refactored.

Tip - ESLint

EsLint should do a good job of flagging when you're dependencies are incorrect. When this occurs with your cursor inside the dependency array you can press "cmd" + "." then select the 'Update the dependencies array to be...' Quick Fix to easily resolve.

Ensure guard clauses are used within conditionals for CMS based UI

Although mitigated through proper use of required fields in the CMS there will be times when client's don't input all required data in fields. If guard clauses are not used properly a page won't error gracefully and will break.

Guard clauses for .map()

Guard clauses for .map() should always check the length of the array being mapped over. e.g.

src/components/HelloDocusaurus.js
...

// Wrong
{
data && data?.map(item, i) => {
...
}
}

// Correct
{
data?.length > 0 && data?.map(item, i) => {
...
}
}

...

Double bang (!!) for UI conditionals

When writing conditionals based on the existence of data always use !! to infer the left side of the condition as a boolean (for non boolean values). This prevents rare instances of 0 being rendered due to the quirks of how truthy/falsy values work in JS.

src/components/HelloDocusaurus.js
...

// Wrong
{
title && (
<p>{data}</p>
)
}

// Correct
{
!!title && (
<p>{data}</p>
)
}

...

Read more about why this occurs in React here.

Be careful when using spread (...) for component props

The spread operator should only be used in instances where the props are not being used within the component they're being spread on and are being passed down in that componenets to an inner child component.

Overusing the spread operator can make it difficult to easily identify the origin of a prop when debugging, HTML elements may also receive non native attributes in the DOM which can lead to errors.

Component directory structure

For simple components that consist of imports, types and a single component definition you can define in a single .tsx named file e.g.

src/components/HelloDocusaurus.js
...

// Directory structure
/Components
ExampleComponent.tsx

// ExampleComponent.tsx
import {useState} from 'React';

interface ExampleComponentProps = {
heading: string;
};

const ExampleComponent = ({heading}: ExampleComponentProps) => {
return(<p>{heading}</p>);
};

export default ExampleComponent;

...

For anything more advanced you should create a named directory with an index.tsx and related inner folders for components, hooks, styles etc. In the below the masthead selector is in the parent index.tsx and within the components folder is all the masthead variants. If all of these were defined in a single Masthead.tsx it would be too much with all the multiple type and component definitions.

src/components/HelloDocusaurus.js
// Directory structure
/Components
/Masthead
index.tsx
/components
GridMasthead.tsx
StaggeredMasthead.tsx


// index.tsx
import GridMasthead from "./components/GridMasthead";
import StaggeredMasthead from "./components/StaggeredMasthead";
import ImageAnimationMasthead from "./components/ImageAnimationMasthead";
import { MastheadProps } from "./types";
import RowMasthead from "./components/RowMasthead";

const Masthead = ({ content, layout }: MastheadProps) => {
const layoutMap = {
"grid-six-items": <GridMasthead content={content} />,
"grid-five-items": <GridMasthead content={content} variant="five" />,
staggered: <StaggeredMasthead content={content} />,
"image-animation": <ImageAnimationMasthead content={content} />,
row: <RowMasthead content={content} />,
};

return layoutMap[layout as keyof typeof layoutMap];
};

export default Masthead;

Managing and navigating named files with multiple type and components definitions is difficult.

Custom hooks

Custom hooks should be used when reusing similar state and logic between components. If you find yourself copying and pasting state and related logic from components to use in others then a custom hook containing all this is most likely more viable.

Custom utility functions

When the same function is defined and used in multiple components it's best to define in one location (utils folder) and import for use.

Map keys

Dont use indexes for map keys - the best thing to do is use unique IDs that persist and come from the CMS, otherwise a template literal incorportaing the index is best.

Aliased imports

Use aliased imports over relative imports. They are better when it comes to refactoring as they won't need to be updated.

Tip

When adding new top level folders update jsconfig at the project root so aliased imports support them.

Classname order

When defining classname strings in object maps for instance try and keep the class names in each attribute/case in the same order for readability - makes going back to this project in a years time just that little bit more palatable e.g.

const getImageClassNames = (i) => {
switch (i) {
case 0:
return "-mt-8 -mr-12 flex justify-end";
case 1:
return "-mt-6 flex justify-end";
case 2:
return "-mt-14 -mr-7 flex justify-end";
default:
return "";
}
};
Tip - CSS Intellisense

This VSCode extension manages the order of class names in component definitions nicely but not inside functions so you'll still have to be wary of the order there.

Default props

Add default props to components whenever possible to avoid errors - in WP ACF for instance fields not populated correctly come in as false by default. Although mitigated through the proper use of required fields in ACF it's still good to avoid the risk.

Semantic HTML

Always try use proper semantic HTML for the element and use case e.g. a heading field from the CMS should never be output in a paragraph - sometimes the type of element is indicated in the Figma file. Read more here

Import Order

Order external packages imports at the top and local components/hooks etc. underneath.

Conditional props

Conditonal based props can be useful - especially when it comes to responsive design. There's always of course ternary prop values as an option but conditional spreading can be useful as well for more advanced use cases/the complete absence of props.

...

const {width: windowWidth} = useWindowSize();

const isSmallDevice = windowWidth < 768;

return(
<TestComponent
// Ternary method
prop = {isSmallDevice ? a : b}
// Spread method
...{
isSmallDevice && {
a,
b
}
}
/>
)

...

Spacing for readability

Be smart about spacing within component logic and component returns for readability. Try group all related lines into blocks - state, custom hooks, animations hooks, var definitions e.g.

...

// This
const CareersScroll = (props: CareersScrollProps) => {
const { heading, content, images } = props;

const scrollRef = useRef(null);
const ref = useRef(null);

const { scrollYProgress } = useScroll({
target: scrollRef,
offset: ["start center", "end end"],
});
const isInView = useInView(ref, { amount: 0.5, once: true });

return (
<div className="careers-scroll">
<div ref={scrollRef} className="relative pb-0 pt-0">

// Not This
const CareersScroll = (props: CareersScrollProps) => {
const { heading, content, images } = props;
const scrollRef = useRef(null);
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: scrollRef,
offset: ["start center", "end end"],
});
const isInView = useInView(ref, { amount: 0.5, once: true });

return (
<div className="careers-scroll">
<div ref={scrollRef} className="relative pb-0 pt-0">

...

Using handlers props as callbacks within components

This can be useful to avoid having to pass in multiple state and setState props into a child component. It's also more flexible as you can handle logic in a components parent and save having to pass in related vars as props as well e.g.

...

// Wrong
// Unneccessary state, vars being passed down to child
const Parent = ({items}) => {
const [activeIndex, setActiveIndex] = useState(0);

return(
<div className='parent'>
{
items?.length > 0 && items?.map((item, i) => (
<Child
key={`item-${i}`}
i={i}
item={item}
setActiveIndex={setActiveIndex}
activeIndex={activeIndex}
/>
))
}
</div>
);

...
};

const Child = ({item, setActiveIndex, activeIndex, i}) => {
const isActive = activeIndex === i;

return(
<button className="child" onClick={() => {setActiveIndex(i)}} >
{item?.title}-{isActive ? "active" : "inactive"}
</button>
)

...
}

// Right
// onClick is more flexible - can easily setup multiple state setters, side effects based on vars
// from the callback (passed in from the child component)
// More descriptive - you can figure out the context of how the state is being used in the child
// component without having to go into it
const Parent = ({items}) => {
const [activeIndex, setActiveIndex] = useState(0);

return(
<div className='parent'>
{
items?.length > 0 && items?.map((item, i) => (
<Child
key={`item-${i}`}
item={item}
isActive={i === activeIndex}
onClick={() => {
setActiveIndex(i);
}}
/>
))
}
</div>
);

...
};

const Child = ({item, isActive, onClick = () => {}}) => {

return(
<button
className="child"
onClick={() => {
onClick?.();
}}
>
{item?.title}-{isActive ? "active" : "inactive"}
</button>
)

...
}

...