Say you are developing a large or complex application where a well-structured codebase can make it easier to understand how the different pieces of the application fit and grow together, while remaining easy to maintain over time.
Is that even possible in real life? 😱 How? 🧐 Answer: Yes, using coding patterns. (Did I just hear a senior chuckle? 🤨) Knowing the common coding patterns in React and when to use them can help structure the code in a way that is more maintainable, scalable, and reusable.
Here are the Ten Commandments (Coding Patterns) in React every React developer should know (and follow, of course. 😉)
#1 Thou shall separate logical from dumb components
…to promote peace (on your code, at least)
aka Container/presentational pattern: This pattern involves separating the stateful “container” components that handle data and logic from the presentational “dumb” components that are responsible for rendering the UI. This can help to improve the reusability, maintainability, and the world peace of your code.
1// presentational/dumb component2function UserList(props) {3 return (4 <ul>5 {props.users.map(user => (6 <li key={user.id}>{user.name}</li>7 ))}8 </ul>9 );10}1112// container/stateful component13function UserListContainer(props) {14 const [users, setUsers] = useState([]);1516 useEffect(() => {17 async function fetchUsers() {18 const response = await fetch('/users');19 const data = await response.json();20 setUsers(data);21 }2223 fetchUsers();24 }, []);2526 return <UserList users={users} />;27}
In this example, the UserList
component is a presentational component that is responsible for rendering a list of users. The UserListContainer
component is a container component that manages the state and logic for fetching the users data. It uses the useEffect
hook to perform an asynchronous fetch request to retrieve the data, and it passes the data down to the UserList
component through props.
This separation of concerns can make it easier to maintain and scale the application, as the presentational component is focused solely on rendering the UI, and the container component is responsible for managing the state and logic. The presentational component can be reused in different contexts without having to worry about the state or logic, and the container component can be easily replaced or updated without affecting the UI.
#2 Thou shall know when to summon the higher-order
for commoners err.. common functionalities
aka Higher-order components (HOCs): Higher-order components are functions that take a component as an argument and return a new component with additional functionality. HOCs can be used to add common functionality, such as authentication, data fetching, or error handling, to multiple components.
1import React, { useState, useEffect } from 'react';23// HOC that adds data fetching functionality to a component4function withData(WrappedComponent) {5 return function WithData(props) {6 const [data, setData] = useState(null);7 const [loading, setLoading] = useState(true);8 const [error, setError] = useState(null);910 useEffect(() => {11 async function fetchData() {12 setLoading(true);13 try {14 const response = await fetch('/data');15 const data = await response.json();16 setData(data);17 setLoading(false);18 } catch (error) {19 setError(error);20 setLoading(false);21 }22 }2324 fetchData();25 }, []);2627 return (28 <WrappedComponent29 data={data}30 loading={loading}31 error={error}32 {...props}33 />34 );35 };36}3738// Presentational component that displays data39function DataList(props) {40 if (props.loading) {41 return <p>Loading data...</p>;42 } else if (props.error) {43 return <p>Error: {props.error.message}</p>;44 } else {45 return (46 <ul>47 {props.data.map(item => (48 <li key={item.id}>{item.name}</li>49 ))}50 </ul>51 );52 }53}5455// Enhanced component that fetches data56const DataListWithData = withData(DataList);5758function MyComponent() {59 return <DataListWithData />;60}
In this example, the withData
HOC adds data fetching functionality to the DataList
component. The withData
HOC uses the useEffect
hook to perform an asynchronous fetch request to retrieve the data, and it passes the data, loading state, and error state down to the DataList
component through props.
The DataListWithData
component is created by calling the withData
HOC with the DataList
component as an argument. When the DataListWithData
component is rendered, it will fetch data from withData
which then renders the DataList
component with the data populated.
#3 Thou shall share thy blessings
(and thy component’s behavior)
aka Render props: The render props pattern is a technique for sharing behavior between components. It involves exposing a prop that is a function that is called with data and returns a JSX element. This allows the component to be flexible and reusable, as the component’s behavior can be customized by the parent component that uses it.
1import React from 'react';23function DataProvider(props) {4 const [data, setData] = useState(null);56 useEffect(() => {7 async function fetchData() {8 const response = await fetch('/data');9 const data = await response.json();10 setData(data);11 }1213 fetchData();14 }, []);1516 return props.render(data);17}1819function MyComponent() {20 return (21 <DataProvider render={data => (22 <div>23 {data ? (24 // Render the data here25 ) : (26 <p>Loading data...</p>27 )}28 </div>29 )} />30 );31}
In this example, the DataProvider
component fetches data from the server and exposes it through a render prop. The MyComponent
component uses the DataProvider
component and provides a function to the render prop that renders the data.
This allows the DataProvider
component to be flexible and reusable, as it can be used in any context where data needs to be fetched and rendered. The MyComponent
component can customize the behavior of the DataProvider
component by providing a different function to the render prop.
Render props are a convenient way to share behavior between components, and they can help to make components more flexible and reusable. They are particularly useful when the behavior of a component needs to be customized based on the context in which it is used.
#4 Thou shall make use of hooks
…it’s a miracle to the React community, really.
Hooks are a way to use state and other React features in functional components. Prior to the introduction of hooks, it was only possible to use state and other React features in class-based components. (cue pails of tears of pre-2019 React devs 😭)
Here’s an example of how to use the useState
hook in a functional component in a React application:
1import React, { useState } from 'react';23function Example() {4 // Declare a state variable called "count"5 const [count, setCount] = useState(0);67 return (8 <div>9 <p>You clicked {count} times</p>10 <button onClick={() => setCount(count + 1)}>11 Click me12 </button>13 </div>14 );15}
In this example, the useState
hook is used to declare a state variable called count
. The useState
hook returns an array with two elements: the current state value, and a function that can be used to update the state value. In this case, the count
variable holds the current value of the state, and the setCount
function is used to update the value of the state.
The useState
hook can be used multiple times in a single component to manage multiple pieces of state.
Here’s an example of how to use the useEffect
hook in a functional component in a React application:
1import React, { useState, useEffect } from 'react';23function Example() {4 const [count, setCount] = useState(0);56 // Similar to componentDidMount and componentDidUpdate7 useEffect(() => {8 // Update the document title using the browser API9 document.title = `You clicked ${count} times`;10 });1112 return (13 <div>14 <p>You clicked {count} times</p>15 <button onClick={() => setCount(count + 1)}>16 Click me17 </button>18 </div>19 );20}
In this example, the useEffect
hook is used to perform an action after the component has been rendered. The useEffect
hook takes a function as an argument, and this function will be called after the component has been rendered. In this case, the function updates the document title using the browser API.
There are many other hooks available in React, and they can be used to perform a variety of tasks such as data fetching, subscribing to events, and managing the lifecycle of a component.
#5 Thou shall only load what shall be loaded
don’t let them users wait for eternity…
aka Lazy loading: Lazy loading is a technique that allows you to defer the loading of a component until it is actually needed. This can be useful in situations where you have a large application with many components, as it can help to improve the performance and loading time of the application by only loading the components that are actually needed.
In React, you can use the React.lazy
function and the Suspense
component to implement lazy loading. The React.lazy
function allows you to import a component that will be loaded asynchronously, and the Suspense
component allows you to specify a fallback UI to display while the component is being loaded.
Here’s an example of how to use React.lazy
and Suspense
to implement lazy loading in a React application:
1import React, { lazy, Suspense } from 'react';23const LazyComponent = lazy(() => import('./LazyComponent'));45function App() {6 return (7 <Suspense fallback={<p>Loading...</p>}>8 <LazyComponent />9 </Suspense>10 );11}
#6 Thou shall split your journey into smaller paths
or the vast ocean that is your codebase into rivers (of joy, I hope)
aka Code splitting: Code splitting is a technique that allows you to split your code into smaller chunks, and only load the code that is needed for a specific route or component. This can help improve the performance of your React application, as it allows you to only load the code that is required for the current view, rather than loading all of the code upfront.
Here is an example of how you can use code splitting in a React application with React Router:
1import React, { Suspense } from 'react';2import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';34const Home = React.lazy(() => import('./routes/Home'));5const About = React.lazy(() => import('./routes/About'));67function App() {8 return (9 <Router>10 <Suspense fallback={<div>Loading...</div>}>11 <Switch>12 <Route exact path="/" component={Home} />13 <Route path="/about" component={About} />14 </Switch>15 </Suspense>16 </Router>17 );18}1920export default App;
In this example, the Home
and About
routes are loaded asynchronously using the React.lazy
and Suspense
components. This allows the code for these routes to be split into separate bundles, which are only loaded when the route is accessed. The Suspense component is used to provide a fallback element that is displayed while the route is being loaded.
You can also use code splitting with the React.lazy
and Suspense
components to split code at the component level, rather than at the route level. For example:
1const MyComponent = React.lazy(() => import('./MyComponent'));23function App() {4 return (5 <Suspense fallback={<div>Loading...</div>}>6 <MyComponent />7 </Suspense>8 );9}
In this case, the MyComponent
component is loaded asynchronously, and the Suspense
component is used to provide a fallback element while the component is being loaded.
#7 Thou shall see the world with patterns
Pattern matching is a technique that allows you to compare a value to a pattern, and execute different code based on whether the value matches the pattern. In React, you can use pattern matching to write more concise and expressive code, particularly when working with props or state that may have multiple possible values.
1function AlertMessages(props) {2 switch (props.type) {3 case 'success':4 return <div style={{ color: 'green' }}>{props.message}</div>;5 case 'error':6 return <div style={{ color: 'red' }}>{props.message}</div>;7 default:8 return <div>{props.message}</div>;9 }10}
In this example, the AlertMessages
component takes a type
prop and a message
prop. The component uses a switch
statement to match the type
prop against different patterns, and returns a different element based on the value of the type
prop.
You can also use the &&
operator to perform pattern matching in React. For example:
1function AlertMessages(props) {2 return (3 <div>4 {props.type === 'success' && (5 <div style={{ color: 'green' }}>{props.message}</div>6 )}7 {props.type === 'error' && (8 <div style={{ color: 'red' }}>{props.message}</div>9 )}10 {props.type !== 'success' && props.type !== 'error' && (11 <div>{props.message}</div>12 )}13 </div>14 );15}
In this example, the AlertMessages
component uses the &&
operator to match the type
prop against different patterns, and returns a different element based on the value of the type
prop.
#8 Thou shall know your states properly
and manage them accordingly 🕵️💁
State is an important concept in React, as it allows you to store and manage data that is specific to a particular component. State allows you to create interactive and dynamic components, as the component’s behavior can change in response to changes in state.
There are several different ways to manage state in a React application. One of the most common approaches is to use the useState
hook, which is a function provided by React that allows you to add state to functional components.
Here is an example of how you can use the useState
hook in a React functional component:
1import { useState } from 'react';23function Counter() {4 // Declare a state variable and a function to update it5 const [count, setCount] = useState(0);67 // Increment the count when the button is clicked8 function handleClick() {9 setCount(count + 1);10 }1112 return (13 <div>14 <p>The count is {count}</p>15 <button onClick={handleClick}>Increment</button>16 </div>17 );18}
In this example, the Counter
component uses the useState
hook to declare a state variable called count
, which is initialized to 0
. It also declares a function called setCount
that can be used to update the value of count.
The Counter
component renders a button that, when clicked, increments the value of count by calling the setCount
function with the new count
value. It also displays the current value of count
in a paragraph element.
You can also use the useState hook to add multiple state variables to a component. For example:
1import { useState } from 'react';23function User() {4 // Declare two state variables and two functions to update them5 const [name, setName] = useState('John');6 const [age, setAge] = useState(30);78 // Update the name when the input value changes9 function handleNameChange(event) {10 setName(event.target.value);11 }1213 // Update the age when the input value changes14 function handleAgeChange(event) {15 setAge(event.target.value);16 }1718 return (19 <div>20 <p>21 Name: <input value={name} onChange={handleNameChange} />22 </p>23 <p>24 Age: <input value={age} onChange={handleAgeChange} />25 </p>26 </div>27 );28}
In this example, the User
component uses the useState hook to declare two state variables: name
and age
. It also declares two functions: setName
and setAge
, which can be used to update the values of the state variables.
The component renders two input elements that are bound to the name and age state variables, and updates the state variables when the input values change.
#9 Thou shall handle blocking tasks in the background
async I have something to do 🧐, await I remember now 😀💡
Asynchronous programming is a way of writing code that performs tasks that may take some time to complete, such as making network requests or reading from a database. In React, you can use asynchronous programming to handle tasks that need to be performed in the background, without blocking the main thread.
Here is an example of how you can use asynchronous programming in a React component:
1import React, { useState, useEffect } from 'react';23function WaterFetcher() {4 const [data, setData] = useState(null);5 const [error, setError] = useState(null);6 const [loading, setLoading] = useState(false);78 // Fetch data from an API when the component mounts9 useEffect(() => {10 async function fetchData() {11 try {12 setLoading(true);13 const response = await fetch('https://wine-api.com/data');14 const json = await response.json();15 setData(json);16 } catch (e) {17 setError(e);18 } finally {19 setLoading(false);20 }21 }22 fetchData();23 }, []);2425 if (loading) {26 return <div>Loading...</div>;27 }2829 if (error) {30 return <div>{error.message}</div>;31 }3233 if (data) {34 return <div>{data.message}</div>;35 }3637 return null;38}
In this example, the WaterFetcher
component uses the useEffect
hook to perform an asynchronous task when the component mounts. The task fetches data from an API using the fetch function, and stores the data in the component’s state using the setData function.
The component also has state variables for error and loading, which are used to store any errors that occur during the fetch, and a flag indicating whether the fetch is currently in progress.
The component renders different elements based on the values of these state variables. If loading is true, it displays a loading message. If error is not null, it displays an error message. If data is not null, it displays the data.
#10 Thou shall normalize data
…or suFf3R thE c0n$eQUençeS 😈
Data normalization is the process of transforming data into a consistent and standardized format. Normalizing data can make it easier to work with and analyze, as it can remove any inconsistencies or errors that may be present in the data.
In React, you can use custom functions or helper libraries to normalize data. Here’s an example of using a custom function to normalize data in a React component:
1import React from 'react';23function normalizeData(data) {4 // Normalize the data here5 return normalizedData;6}78function MyComponent(props) {9 const normalizedData = normalizeData(props.data);1011 return (12 // Render the normalized data here13 );14}
Here’s an example of using a helper library to normalize data in a React component:
1import React from 'react';2import normalize from 'normalize-data-library';34function MyComponent(props) {5 const normalizedData = normalize(props.data);67 return (8 // Render the normalized data here9 );10}
It’s also possible to use a combination of custom functions and helper libraries to normalize data in a React component.
Keep in mind that the specific steps involved in normalizing data will depend on the format of the data and the desired output.
Data normalization is often an iterative process, as you may need to perform multiple steps in order to fully normalize the data. It’s also a good idea to validate the normalized data to ensure that it is accurate and consistent.
So in summary (or if you scrolled all the way down and didn’t read 😠):
- Thou shall separate logical from dumb components: Container/presentational
- Thou shall know when to summon the higher-order: Higher-Order Components
- Thou shall share thy blessings and thy components behavior: Render Props
- Thou shall make use of hooks: Hooks, duh.
- Thou shall only load what shall be loaded: Lazy Loading
- Thou shall split your journey into smaller paths: Code Splitting
- Thou shall see the world with patterns: Pattern Matching
- Thou shall know your states properly: State Management
- Thou shall handle blocking tasks in the background: Asynchronous Fetching
- Thou shall normalize data: Data Normalization
You’ve just seen the surface. There are more advanced patterns you can use for developing apps in React and modern web apps in general. The world is your oyster, lots of pearls out there.
I hope you enjoyed reading and feel free to leave some comments below if you oppose to these commandments or whatnot.