DEV Community

loading...
Cover image for React Best Practices

React Best Practices

awedis profile image awedis Updated on ・3 min read

In this article, I want to share some details to take care of, which will make us better React developers


Separate logic from JSX
  • It's always good to spread the logic (or functional part) from the JSX, the code will be cleaner and more editable, check the example below how to handle an onClick event instead of putting it directly inside the element
import React from 'react';

function Todo() {
  let condition = true;

  const addHandler = () => {
    if (condition) {
      //do api call
    }
  }

  return (
    <div>
      <button
        onClick={() => addHandler()}
      >Add</button>
    </div>
  )
}

export default Todo;
Enter fullscreen mode Exit fullscreen mode
Split into small components & make them reusable
  • In this part let's try to split a UI component from a real project c5781d72c1298dc869b74702b4ee42a0
  1. NavBar: Top navbar container which contains the HamBtn
  2. HamBtn: The 3 horizontal lines button, that can be used in other parts
  3. TodoRow: Todo row, containing the text, and other action buttons
  4. RadioBtn: Toggle button
  5. DeleteBtn: Delete button for todo
  6. Button: Generic button component with several dynamic props
  7. Container: The whole container for the Todo list.

All these will be separate components, this will help us in the long run, if the project becomes bigger almost all the components can be reused 😎

State Management (Redux or Context)
  • In the case of using Redux Library, I highly recommend using Sagas which will help you to make async API calls
    Redux: Perfect for larger applications where there are high-frequency state updates

  • And for Context Api, its much simple than the Redux, and you don't need to download any library
    Context API: Resourceful and ideal for small applications where state changes are minimal

Hooks and Functional Components
  • No more "this"
  • Fewer lines of code
  • It's easier to debug, test & refactor it

I think the majority of developers are already using all their projects based on these two, but I just wanted to mention it 😁

Styled-Components
  • Styled-Components is one of the new ways to use CSS in modern JavaScript
  • It is meant to be a successor of CSS Modules, a way to write CSS that's scoped to a single component, and not leak to any other element on the page
import React from 'react';
import { Text } from './SubTitle.styled';

function SubTitle() {
  return (
    <Text>Hello</Text>
  )
}
export default SubTitle;
Enter fullscreen mode Exit fullscreen mode
import styled from "styled-components";

export const Text = styled.span`
  color: #AAA;
  font-size: 20px;
`;
Enter fullscreen mode Exit fullscreen mode
Testing
  • Unit Testing - (to check a single component of an application, for more critical functions)
  • Integration Testing - (to check if different pieces of the modules are working together)
  • End-to-End Testing - (involves testing an application's workflow from beginning to end, aims to replicate real user scenarios)
Typescript

TypeScript is a “typed superset of JavaScript that compiles to plain JavaScript.”

Using Typescript in React will help you to develop more stable components, that are strongly typed and are faster to be integrated, lets check the simplest example

interface Props {
  label: string;
  onClick: () => void;
}
Enter fullscreen mode Exit fullscreen mode
function Button({ label, onClick, ...props }: Props) {
  return (
    <button
      onClick={onClick}
      {...props}
    >
      {label}
    </button>
  )
}

export default Button;
Enter fullscreen mode Exit fullscreen mode

12345

Intrinsic elements:
Intrinsic elements are looked up on the special interface JSX.IntrinsicElements. By default, if this interface is not specified, then anything goes and intrinsic elements will not be type checked. However, if this interface is present, then the name of the intrinsic element is looked up as a property on the JSX.IntrinsicElements interface.

Intrinsic elements will allow us to use the native 'props' of an element

export type Props = JSX.IntrinsicElements["button"] & {
  label: string;
}
Enter fullscreen mode Exit fullscreen mode
<Button
  label={'SUBMIT'}
  onClick={() => console.log('CLICK')}
  onBlur={() => console.log('BLUR')}
/>
Enter fullscreen mode Exit fullscreen mode

React is an awesome library, you can split & organize your code in a way to become very flexible & scalable, wanted to keep it simple & high-level

Wish it was helpful and that's it 😊

Discussion (10)

pic
Editor guide
Collapse
lukeshiru profile image
△ LUKE知る

Nice article. I specially agree with the part about using Functional Components instead of classes. I could argue that instead of "React best practices" this could be titled just "React Practices", because some of the practices mentioned here might be consider bad on some environments, but I'll just focus in the TS example.

First, ideally you should have the props in an external file, something like ButtonProps.ts. Also you can extend the native button, making it way more useful (JSX.IntrinsicElements["button"]):

export type ButtonProps = JSX.IntrinsicElements["button"] & {
  primary?: boolean;
};
Enter fullscreen mode Exit fullscreen mode

Second, you can use FC from react to have better types:

import type { FC } from "react";
import type { ButtonProps } from "./ButtonProps";
import classnames from "classnames";

export const Button: FC<ButtonProps> = ({
  className,
  primary = false,
  ...props
}) => (
  <button
    className={classnames({ primary }, className)}
    {...props}
  />
);
Enter fullscreen mode Exit fullscreen mode

The more you "extend the native elements", the more predictable and easy to use your components are. Ideally you should also use stuff like memo and forwardRef as well :D

Collapse
fnky profile image
Christian Petersen • Edited

There's a small caveat with using JSX.IntrinsicElements, which is that it always includes a ref prop, even though it won't work in reality. This type is also referring to a legacy type for class-based components (LegacyRef<T>).

The better choice would be to use React.[Element]HTMLAttributes<HTML[Element]Element>, where [Element] is replace with the element type that your component wraps over, or the more explicit ComponentPropsWithRef<"button"> / ComponentPropsWithoutRef<"button">.

I prefer the former, since you don't have to think about updating the type of your Props in case you either wrap or unwrap your component forwardRef, which will return the correct type.

To explain further: Using JSX.IntrinsicElements will give you incorrect type information for props:

type Props = JSX.IntrinsicElements["button"]
const Button = (props: Props) => <button {...props} />;
const usage = <Button ref={...} /> // ref?: LegacyRef<HTMLButtonElement>
Enter fullscreen mode Exit fullscreen mode

Passing anything to ref will throw a warning in development mode:

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Enter fullscreen mode Exit fullscreen mode

This is because it has not been forwarded by our wrapper component.

It also refers to the incorrect type LegacyRef<T> which accepts string which are legacy as mentioned in the Ref and the DOM.

It's by built-in JSX elements to be backwards-compatible with earlier versions of the React API.

However, using the React.[Element]HTMLAttributes<HTML[Element]Element> instead:

type Props = React.ButtonHTMLAttributes<HTMLButtonElement>;
const Button = (props: Props) => <button {...props} />;
const usage = <Button ref={...} /> // no ref prop on Button, which is correct.
Enter fullscreen mode Exit fullscreen mode

Gives us the correct type information; that ref is not present since it doesn't take any refs.

If you'd like to accept a ref and pass it down to the underlying element, you want to use forwardRef:

type Props = React.ButtonHTMLAttributes<HTMLButtonElement>;
const Button = React.forwardRef<HTMLButtonElement, Props>((props, forwardedRef) => {
  return <button ref={forwardedRef} {...props} />;
});
const usage = <Button ref={...} /> // ref?: Ref<HTMLButtonElement>
Enter fullscreen mode Exit fullscreen mode

Note that the type for ref prop is now Ref<T>, which conforms correctly to what useRef and createRef returns (RefCallback<T> | RefObject<T>).

Hope this helps :-)

Collapse
pavermakov profile image
Pavel Ermakov

How do test components with hooks?

Collapse
lukeshiru profile image
△ LUKE知る

If you want the 2021 answer: Jest and React Testing Library for unit testing, and Cypress for e2e!

For old/legacy projects, it might be useful to learn enzyme as well, but if you're creating something from scratch, use modern tools like those instead :D

Collapse
jwhenry3 profile image
Justin Henry

There are some complex interactions that might be just outside of automated testing, so I personally use Storybook so I can manually test hard-to-reach edge cases.

This especially helps when you need a sandbox of real-world state (like connecting to apis and websockets to run through production scenarios without impacting deployments or full-blown application e2e)

Collapse
awedis profile image
awedis Author

Check Jest & Enzyme Libraries, Both are specifically designed to test React applications :)

Collapse
yagoroliveira profile image
Yago R. Oliveira

One question, what you think about Context API with React Query Library and Unstated-next for a large application?

Collapse
awedis profile image
awedis Author • Edited

Used before Context API with GraphQL though, the code was much simpler and cleaner, I believe in order to choose which state management to use we need to check the whole app state & how much we have global data. Yep and regarding the Unstated-next it's a good library that can be integrated with Context API.

Collapse
jwhenry3 profile image
Justin Henry • Edited

I'd advocate for a provider-less state tool like zustand (given the state is global)
github.com/pmndrs/zustand
Though, I can see the benefit of the provider pattern if you want multiple scoped states...

React query is great, and keep in mind there is also SWR
swr.vercel.app/

EDIT: After looking at unstated-next, I heavily support its usage in cases where you want multiple instances of a state (maybe you have separate thread management for a messaging app, and you want those states isolated).
I push for zustand for global state and unstated-next for localized state.

Collapse
abror1997 profile image
Abror Xalilov

Interested