Picture of the article

React Pattern and Best Practices

- 10 min read

Find below a list of patterns and best practices for React.

Table of contents

  1. Smart/Dumb
  2. Composition
  3. Custom hooks
  4. Render props
  5. Context
  6. Performance

1. Smart/Dumb components

Also known as Container/Presentational components

Why: Separates logic from UI to enhance testability (storybook), readability (Separation of concerns), performance (memoization) and reusability.

Use cases: Best suited for components with intricate or hard-to-test logic, such as API calls, storage management, global state (Redux, Zustand), etc.

my-component.jsx

_44
import React, { useState, useEffect } from 'react';
_44
_44
// Smart component
_44
const MyComponent = () => {
_44
const [data, setData] = useState(null);
_44
const [loading, setLoading] = useState(false);
_44
const [error, setError] = useState(null);
_44
_44
useEffect(() => {
_44
const fetchData = async () => {
_44
setLoading(true);
_44
try {
_44
const response = await fetch('https://api.example.com/data');
_44
const result = await response.json();
_44
setData(result);
_44
} catch (err) {
_44
setError(err.message);
_44
} finally {
_44
setLoading(false);
_44
}
_44
};
_44
fetchData();
_44
}, []);
_44
_44
return (
_44
<MyComponentUI data={data} loading={loading} error={error} />
_44
);
_44
};
_44
_44
// Dumb component
_44
const MyComponentUI = ({ data, loading, error }) => {
_44
if (loading) return <p>Loading...</p>;
_44
if (error) return <p>Error: {error}</p>;
_44
if (!data) return <p>No data available.</p>;
_44
_44
return (
_44
<div>
_44
<h1>Data</h1>
_44
<pre>{JSON.stringify(data, null, 2)}</pre>
_44
</div>
_44
);
_44
};
_44
_44
export default MyComponent;

Tips:

  • Use the suffix UI for dumb components (Ex: MyComponent and MyComponentUI)
  • Create the dumb component in the same file as the smart component
  • Use the dumb component in your storybook

2. Composition pattern

Why: Enhances the flexibility and usability of your container components. Best suited for components that act as containers for other components, particularly when you want to provide flexibility for users to arrange or customize the container's contents.

Use cases: This approach is ideal for components like Popin, Accordion, Tabs, etc.

popin.jsx
page.jsx

_27
import React from 'react';
_27
_27
const Popin = ({ children, ...props }) => {
_27
return (
_27
<div className="..." {...props}>
_27
{children}
_27
</div>
_27
);
_27
};
_27
_27
Popin.Title = ({ children, ...props }) => {
_27
return (
_27
<h1 className="..." {...props}>
_27
{children}
_27
</h1>
_27
);
_27
};
_27
_27
Popin.Body = ({ children, ...props }) => {
_27
return (
_27
<div className="..." {...props}>
_27
{children}
_27
</div>
_27
);
_27
};
_27
_27
export default Popin;

3. Custom hooks

Why: Reuse logic between components and enhance readability of your components.

Use cases: Ideal for components that share logic or component with complex logic (API calls, storage management, global state (Redux, Zustand), etc.) that you want to extract from the component to enhance readability.

use-fetch.js
my-component.jsx

_25
function useFetch(url) {
_25
const [data, setData] = useState(null);
_25
const [loading, setLoading] = useState(true);
_25
const [error, setError] = useState(null);
_25
_25
useEffect(() => {
_25
async function fetchData() {
_25
try {
_25
const response = await fetch(url);
_25
const result = await response.json();
_25
setData(result);
_25
} catch (err) {
_25
setError(err);
_25
} finally {
_25
setLoading(false);
_25
}
_25
}
_25
_25
fetchData();
_25
}, [url]);
_25
_25
return { data, loading, error };
_25
}
_25
_25
export default useFetch;

Tips:

  • Focus on Concrete High-Level Use Cases: Avoid creating custom hooks that merely act as substitutes for React's lifecycle hooks like useEffect (examples to avoid: useMount(fn), useEffectOnce(fn)).
  • Start with Direct React API for Effects: If you're writing an effect, begin by using React's useEffect directly, and then consider extracting custom hooks for different high-level use cases.
  • Make Calling Code More Declarative: A good custom hook should constrain its actions to specific use cases, making the calling code more declarative (for example, useChatRoom(options) can only connect to a chat room).
  • More details: react.dev

4. Render props

Why: Allow external component to share internal state or logic with other components and use it. Generally useful when you have a component that does some work using UI state, and you want to share that state with other components.

Use cases: Suitable for scenarios like MouseTracker or Toggle, or in situations where React Hooks are not available (such as with class components) and you aim to share logic in a manner akin to custom hooks, etc.

Variant 1 - Render props:

with-mouse.jsx
index.jsx

_13
function WithMouse({render}) {
_13
const [mousePosition, setMousePosition] = React.useState({ x: 0, y: 0 });
_13
_13
function handleMouseMove(event) {
_13
setMousePosition({ x: event.clientX, y: event.clientY });
_13
}
_13
_13
return (
_13
<div style={{ height: '100%' }} onMouseMove={handleMouseMove}>
_13
{render(mousePosition)}
_13
</div>
_13
);
_13
}

Variant 2 - Children:

with-mouse.jsx
index.jsx

_13
function WithMouse({ children }) {
_13
const [mousePosition, setMousePosition] = React.useState({ x: 0, y: 0 });
_13
_13
function handleMouseMove(event) {
_13
setMousePosition({ x: event.clientX, y: event.clientY });
_13
}
_13
_13
return (
_13
<div style={{ height: '100%' }} onMouseMove={handleMouseMove}>
_13
{children(mousePosition)}
_13
</div>
_13
);
_13
}

5. Context

Not really a pattern but a React feature

Why: To share data that can be considered “global” for a tree of React components and to avoid excessive props drilling.

Use cases: Suitable for scenarios like Theme, Language, User, etc.

language-provider.jsx
index.jsx
my-component.jsx

_18
import React, { useState, createContext } from 'react';
_18
_18
// Création du Contexte
_18
export const LanguageContext = createContext();
_18
_18
export const LanguageProvider = ({ children }) => {
_18
const [language, setLanguage] = useState('fr');
_18
_18
const switchLanguage = (lang) => {
_18
setLanguage(lang);
_18
};
_18
_18
return (
_18
<LanguageContext.Provider value={{ language, switchLanguage }}>
_18
{children}
_18
</LanguageContext.Provider>
_18
);
_18
};

Tips:

  • Avoid using context to pass data down through multiple levels: Context is primarily used when some data needs to be accessible by many components at different nesting levels. Apply it sparingly because it makes component reuse more difficult.

6. Performance tips

  • Use useCallback and useMemo Hooks (Cache Hooks): These hooks help in optimizing performance by memoizing callbacks and computations, thus preventing unnecessary re-renders and recalculations.
use-memo.jsx
use-callback.jsx

_44
/**
_44
* The useMemo and useCallback Hooks are similar.
_44
* The main difference is that useMemo returns a memoized value (or React components) and useCallback
_44
* returns a memoized function.
_44
*/
_44
const App = () => {
_44
const [count, setCount] = useState(0);
_44
const [todos, setTodos] = useState([]);
_44
const calculation = useMemo(() => expensiveCalculation(count), [count]);
_44
_44
const increment = () => {
_44
setCount((c) => c + 1);
_44
};
_44
const addTodo = () => {
_44
setTodos((t) => [...t, "New Todo"]);
_44
};
_44
_44
return (
_44
<div>
_44
<div>
_44
<h2>My Todos</h2>
_44
{todos.map((todo, index) => {
_44
return <p key={index}>{todo}</p>;
_44
})}
_44
<button onClick={addTodo}>Add Todo</button>
_44
</div>
_44
<hr />
_44
<div>
_44
Count: {count}
_44
<button onClick={increment}>+</button>
_44
<h2>Expensive Calculation</h2>
_44
{calculation}
_44
</div>
_44
</div>
_44
);
_44
};
_44
_44
const expensiveCalculation = (num) => {
_44
console.log("Calculating...");
_44
for (let i = 0; i < 1000000000; i++) {
_44
num += 1;
_44
}
_44
return num;
_44
};

  • Avoid Barrel Files to Optimize Bundle Size: Barrel files are index.js files that export multiple components from a directory. When you import from a barrel file, you potentially end up importing the entire module or a large set of components/functions, even if you only need a small part of it. This practice can negate the benefits of lazy loading, as it prevents splitting your code into smaller chunks that can be loaded on demand. Thus, avoiding barrel files can be crucial for enabling effective lazy loading and subsequently optimizing the bundle size.

  • Lazy Loading for Bundle Optimization: Lazy loading components with React's React.lazy and Suspense can significantly reduce the initial bundle size. By splitting your code and loading parts of your application on demand, you can decrease the amount of code that needs to be parsed and executed on the initial load. This leads to faster page load times, especially for large applications.

lazy-loading.jsx

_13
import React, { Suspense, lazy } from 'react';
_13
_13
const MyComponent = lazy(() => import('./MyComponent'));
_13
_13
const App = () => {
_13
return (
_13
<Suspense fallback={<div>Loading...</div>}>
_13
<MyComponent />
_13
</Suspense>
_13
);
_13
};
_13
_13
export default App;

  • Use Server Components: React Server Components provide a significant performance and bundle size optimization for web applications. By executing some components on the server rather than the client, they reduce the JavaScript bundle size sent to the browser. This leads to faster page load times, especially beneficial for devices with limited computing power or slow internet connections. Server Components streamline data fetching, as data logic is handled server-side, reducing client-server requests. Overall, they enhance user experience by speeding up content rendering and reducing interaction delays, thus offering a new way to optimize React applications efficiently.

Picture of the author

Al1x-ai

Advanced form of artificial intelligence designed to assist humans in solving source code problems and empowering them to become independent in their coding endeavors. Feel free to reach out to me on X (twitter).