React Native has continued to evolve and mature since its inception, and as we step into 2023, it’s essential to keep up with the latest React Native best practices.
Let’s explore some the top strategies I’ve found when building React Native applications to this date.
1. Embrace Functional Components with Hooks
Functional components, combined with hooks, have become the preferred way to write React Native components. Hooks allow you to use state and other React features without having to write a class component.
Here’s an example of a simple class component in React Native:
import React from 'react';
import { Button, View, Text } from 'react-native';
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
incrementCount = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<View>
<Text>You clicked {this.state.count} times</Text>
<Button onPress={this.incrementCount} title="Click me" />
</View>
);
}
}
This component becomes :
import React, { useState } from 'react';
import { Button, View, Text } from 'react-native';
const MyComponent = () => {
const [count, setCount] = useState(0);
const incrementCount = () => {
setCount(count + 1);
}
return (
<View>
<Text>You clicked {count} times</Text>
<Button onPress={incrementCount} title="Click me" />
</View>
);
};
2. Optimized Performance
Performance optimization is crucial in React Native development. Below are a few tips that can help improve your application performance:
Use Memoization Techniques
Memoization is a programming technique used to optimize computer programs by storing the results of expensive function calls and reusing them when the same inputs occur again. This technique is useful when a function is called multiple times with the same arguments, as it saves time by avoiding re-computation of the same results.
React provides you with some functions to use memoization in your components. You can prevent unnecessary renders by using React.memo()
, useMemo()
, and useCallback()
.
Let’s have a look at some examples each one of them :
1. React.memo()
React.memo()
is a higher order component that memoizes your functional component and only re-renders it if the props have changed.
const MyComponent = (props) => {
// Your component
}
const MyMemoizedComponent = React.memo(MyComponent);
// MyMemoizedComponent will only re-render if props change.
2. useMemo()
useMemo()
is a hook that returns a memoized value. It takes two arguments: a function and a list of dependencies. The function will only be re-computed when one of the dependencies has changed.
import React, { useMemo } from 'react';
const MyComponent = (props) => {
const computeExpensiveValue = (a, b) => {
// some expensive computation...
};
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// Render using `memoizedValue`...
};
3. useCallback()
useCallback()
is a hook that returns a memoized version of the callback function that only changes if one of the dependencies has changed.
import React, { useState, useCallback } from 'react';
const MyComponent = () => {
const [count, setCount] = useState(0);
const incrementCount = useCallback(() => {
setCount(count + 1);
}, [count]);
// Render using `incrementCount`...
};
In the above example, incrementCount
will only change if count
changes. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (for example, shouldComponentUpdate).
LazyLoad Components
You can implement lazy loading to boost startup times. React’s built-in React.lazy()
function makes this easy.
The React.lazy() function lets you render a dynamic import as a regular component. It is used for code splitting in a React web app and is useful for reducing the size of the main bundle and delaying loading code until itโs needed.
As of 2023, React.lazy()
can be used in React Native for better performance.
Here is an example of how to use it in React Native:
import React, { lazy, Suspense } from 'react';
import { View, Text } from 'react-native';
const LazyLoadedComponent = lazy(() => import('./SomeComponent'));
export default function App() {
return (
<View>
<Text>Loading...</Text>
<Suspense fallback={<Text>Loading...</Text>}>
<LazyLoadedComponent />
</Suspense>
</View>
);
}
In this snippet, React.lazy()
is used to load a component only when it’s needed. The Suspense
component is used to display some fallback content (in this case, a loading text) while the lazy-loaded component is loading.
This can make a huge difference if your app contains a lot of screens and you are trying to improve your app startup time.
Offload tasks to background threads
Use libraries such as react-native-threads
to handle intensive tasks on a background thread.
Here is an example of how to use it:
import { Thread } from 'react-native-threads';
// Start a new React Native JavaScript process
const thread = new Thread('path/to/thread.js');
// Send a message, strings only
thread.postMessage('hello');
// Listen for messages coming from the thread
thread.onmessage = (message) => console.log(message);
// Remember to terminate the thread when you're done
thread.terminate();
In a separate JavaScript file (path/to/thread.js
), you would handle incoming messages and do work on the background thread:
self.onmessage = (message) => {
console.log(`Received message from main thread: ${message}`);
// Do some work...
// Send a message back to main thread
self.postMessage('Hello from thread');
};
This can be a useful way to offload intensive tasks to a background thread, and this way keep the UI thread free for rendering and user interaction.
3. Consistent Coding Style
Adhering to a consistent coding style is a must. Use ESLint along with Prettier for automatic linting and code formatting. This practice not only makes your code more readable but also helps in identifying potential problems in your codebase.
# Install ESLint and Prettier
npm install --save-dev eslint prettier
4. Use of Type Checking
TypeScript has gained immense popularity in the React Native community. It provides static type checking, which can prevent potential runtime errors.
If you haven’t started using TypeScript with React Native, 2023 is a great year to start.
Here’s an example of how to define a functional component in React Native with TypeScript. We define a type for the component props and use it in the function signature.
Please also note that in this example we use the useState
hook. We also define a type for the state and use it in the call to useState
:
import React from 'react';
import { View, Text } from 'react-native';
type MyComponentProps = {
name: string;
};
type CounterState = number;
function MyComponent({ name }: MyComponentProps) {
const [count, setCount] = useState<CounterState>(0);
const increment = () => setCount(count + 1);
return (
<View>
<Text>Hello, {name}!</Text>
<Text>You clicked {count} times</Text>
<Button onPress={increment} title="Click me" />
</View>
);
}
export default MyComponent;
The use of types helps to ensure that your code is correct and makes it easier to understand and refactor.
5. Automated Testing
Testing is a crucial aspect of any application development process. Automated tests help to ensure the correctness of your code, make it easier to refactor, and can prevent regressions.
They can also improve the design of your software by encouraging you to write testable code.
Here are some key points to consider when testing your React Native applications.
5.1 Jest
Jest is the recommended testing framework for React Native applications. It’s a zero-configuration testing platform that works out of the box with React Native. You can use it to write unit tests, snapshot tests, and more.
Here’s an example of a simple unit test in Jest:
import React from 'react';
import { render } from '@testing-library/react-native';
import MyComponent from './MyComponent';
test('renders correctly', () => {
const { getByText } = render(<MyComponent name="React Native" />);
expect(getByText(/React Native/i)).toBeTruthy();
});
We’re using the render
function from React Native Testing Library to render MyComponent
, and then we’re using Jest’s expect
function to assert that the text “React Native” is in the document.
5.2 React Native Testing Library
The React Native Testing Library is a great tool for writing integration tests that simulate how your users interact with your application. It encourages you to write tests that focus on the user’s perspective rather than the implementation details of your components.
Here’s an example of a test that simulates a user pressing a button:
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import MyComponent from './MyComponent';
test('increments counter on button press', () => {
const { getByText } = render(<MyComponent />);
const button = getByText('Increment');
fireEvent.press(button);
expect(getByText('Count: 1')).toBeTruthy();
});
In this test, we’re simulating a button press with the fireEvent.press
function and then asserting that the text on the screen updates correctly.
5.3 End-to-End Testing with Detox
For end-to-end testing, Detox is a popular choice in the React Native community. Detox tests run on a device or emulator, just like your app, and simulate real user interactions.
Here’s an example of a simple Detox test:
describe('App', () => {
beforeEach(async () => {
await device.reloadReactNative();
});
it('should display welcome message', async () => {
await expect(element(by.id('welcome'))).toBeVisible();
});
});
Here, we’re reloading the app before each test and then asserting that the element with the id “welcome” is visible on the screen.
6. Continuous Integration/Continuous Deployment (CI/CD)
Continuous Integration (CI) and Continuous Deployment (CD) are practices that aim to improve the software development process by making it more automated, reliable, and efficient.
Setting up CI for your React Native app can help you catch bugs and errors before they make their way to production. When setting up CI, it’s a good idea to run your test suite, check your code for linting errors, and build your app to ensure it compiles correctly.
6.1 Continuous Integration (CI)
Continuous Integration (CI) is a practice where developers integrate code into a shared repository frequently, usually multiple times per day. Each integration can then be verified by an automated build and automated tests that are triggered when developers commit to the repository.
Services like GitHub Actions, GitLab CI/CD, and Bitrise are commonly used for setting up CI/CD pipelines for React Native apps.
Here are some key points to remember when setting up CI for your React Native project:
- Automated Testing: As discussed in the previous section, automated tests are a crucial part of CI. Each time code is pushed to your repository, your CI server should run your entire test suite to catch any potential bugs or regressions.
- Linting and Static Analysis: Linting and static analysis tools can catch common programming errors, enforce coding standards, and even detect some types of security vulnerabilities. You can run tools like ESLint and TypeScript as part of your CI process.
- Building: To ensure that your code compiles correctly, your CI server should build your React Native application for both iOS and Android.
6.2 Continuous Deployment (CD)
Continuous Deployment (CD) is the practice of automatically deploying new versions of your software to production as soon as changes are made and all tests pass. This requires a high level of automation and a robust suite of tests.
For a React Native application, CD might involve automatically building and deploying updates to your app on the Apple App Store and Google Play Store.
Here are some things to consider when setting up CD for your React Native project:
- Automated Deployment: Tools like EAS CLI and Fastlane can automate many aspects of the deployment process, including code signing, building, and uploading your app to the store. They can also automatically manage your app’s screenshots, metadata, and more.
- Feature Flags: Feature flags allow you to control the rollout of new features. You can turn features on and off without deploying a new version of your app, and you can even enable features for specific users or groups of users. You can handle these feature flag in your backend or use a tool like ConfigCat
- Monitoring and Analytics: Once your app is deployed, you’ll need to monitor its performance and usage. Tools like Sentry, Firebase, and AppCenter can provide crash reporting, analytics, and more.
By implementing CI/CD practices in your React Native development process, you will catch bugs earlier, deploy updates faster to you users, and ultimately deliver a better experience to them.
7. Effective State Management
In React Native, as in any React-based application, state is used to store information that can change over time or between different views of your app.
How you manage your app’s state can have a significant impact on its complexity, performance, and ease of testing.
Redux (with Redux-toolkit) has been a long-standing popular choice, but libraries like MobX, Recoil and Zustand are also worth considering.
Context API combined with hooks can also be an excellent choice for smaller applications or specific parts of larger applications.
import React, { createContext, useContext, useState } from 'react';
const GlobalStateContext = createContext();
const GlobalStateProvider = ({ children }) => {
const [state, setState] = useState(0);
return (
<GlobalStateContext.Provider value={[state, setState]}>
{children}
</GlobalStateContext.Provider>
);
};
const useGlobalState = () => useContext(GlobalStateContext);
// Now the useGlobalState hook can be used in any functional component to access the state.
Here at ReactNativeCentral.com, we really like Zustand ๐!
Zustand is an tiny library, extremely easy to use and doesn’t require a lot of boilerplate code. Here is small example of how to use Zustand in React Native.
First, let’s create a store with Zustand:
import create from 'zustand';
// Defining the store
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
}));
In this example, we’re creating a simple store that holds a count
state and an action increment
.
Next, we create a component that uses this store:
import React from 'react';
import { Button, Text, View } from 'react-native';
import useStore from './path/to/my/store';
const Counter = () => {
const count = useStore(state => state.count);
const increment = useStore(state => state.increment);
return (
<View>
<Text>You clicked {count} times</Text>
<Button title="Increment" onPress={increment} />
</View>
);
};
export default Counter;
In this component, we’re using the useStore
hook to access the count
and increment
from our Zustand store. When the “Increment” button is pressed, the increment
action is dispatched. The count
is displayed in a Text
component.
This is a simple example, but Zustand can handle much more complex state management needs as well. It supports asynchronous actions, middlewares, devtools, and more.
Wrapping up
React Native is revolutionizing the way we build mobile applications, bringing the efficiency and flexibility of React and JavaScript to the mobile development world.
However, to truly leverage the power of React Native, it’s crucial to follow best practices, especially when working in a team.
From adhering to established coding standards with ESLint and Prettier, to optimizing performance with memoization, and making the most of features like React.lazy()
and react-native-threads
; the best practices of 2023 extend and improve upon those that have been established in the React Native community over the years.
Understanding and effectively utilizing TypeScript in your React Native projects, employing a solid testing strategy, setting up a robust CI/CD pipeline, and mastering state management are all very important when building high-quality, maintainable, and scalable React Native apps.
These best practices will serve you as a valuable guide when developing performant and reliable mobile applications.
These are general recommendations, so always consider what makes the most sense for your specific project or for your team.
Thank you for reading, I hope you learned something and looking forward to seeing you in the next post!
To further your understanding of React Native, have a look at some of our other articles: