State Management in React Native with Redux and Redux Thunk
The biggest challenge you will face when building a React Native application is managing state predictably across every component. With so many available NPM libraries that address the problem of state management, such as Redux and MobX , and built-in APIs for features like React Context , architecting a robust state management solution has never been easier. However, if we don't fully understand how these technologies work, then we may end up littering the application state with too much state and triggering unnecessary re-renders from extra state changes. The key to state management is distinguishing between two types of state: component and application. Take any one component from your application, and for each state variable, ask yourself whether it belongs only within this component or if at least one other component depends on it. State isolated to a single component should be handled with the useState hook or passed from a parent component via props , whereas state shared by multiple components should be handled by React Context or a third-party library like Redux. While you can manage state on the native side, the transmission of data across the React Native bridge and serialization/deserialization of data per operation is neither optimal nor worthwhile. You should just keep the application state on the JavaScript side. Proper state management accomplishes a few things for the codebase: Despite the React Context API, the first-party, built-in, lightweight state management solution, you shouldn't immediately write off Redux for managing application state. You may reject Redux because of the additional boilerplate code it adds to your application, but Redux enforces a unidirectional flow of data and strict patterns for manipulating application state. In a team environment, Redux provides a superior developer experience with time-travel debugging tools ( Redux DevTools ) for finding bugs earlier in development and state snapshots for quickly reproducing bugs. For React Native applications, Redux middleware lets us execute side-effects, such as Google Analytics event tracking and asynchronous API calls, without blocking state updates. Middleware extends Redux's capabilities, and they can be chained together to perform complex asynchronous logic and interactions. Writing middleware is easy with Redux Thunk , which dispatches actions asynchronously by delaying the dispatch with thunks. Typically, an action creator looks like this: When called, the action creator returns an action object. The action can be dispatched synchronously to a Redux store's reducer like so: With thunks , the action creator returns a function that performs the dispatch asynchronously. For example, what if we needed to persist some data to a Redis cache prior to updating the Redux store? Sending a request to an API endpoint that caches data in a Redis instance is an asynchronous action. By wrapping code within a function to delay its immediate invocation, thunks elegantly runs asynchronous logic while still being able to interact with the store. Best of all, we can dispatch thunk-based actions as if they were standard actions! Below, I'm going to show you how to integrate Redux and Redux Thunk into a React Native application from scratch. To get started, initialize a new React Native project using the TypeScript template : Within the root of the project directory, install the Redux ( redux ) and Redux Thunk ( redux-thunk ) libraries and their type definitions from the npm registry by running these commands: react-redux provides React bindings for Redux, and redux-logger logs Redux actions and snapshots the state before and after the action is dispatched and handled by the reducer. Now, create the following directories: Anytime you introduce a new component that interacts with the Redux store, structure the component in the following way: This approach loosely couples Redux with the component. Since the component receives the data and methods via its props, they can hypothetically come from anywhere. If you decide to remove Redux from your application or move the component to its own library (or to an application without Redux), then the component will still work properly as long as it receives the data and methods from a parent component. Let me show you how this works with an example. Suppose we add a <Counter /> component that displays the current count stored in the Redux store and features two buttons for incrementing and decrementing the count by one. Nothing fancy. Start by writing the store, reducers and action creators. To initialize a new store, call Redux's createStore function. ( shared/redux/store/index.ts ) Although redux-logger is production-ready, the overhead of logging on each action may adversely affect performance based on the number of logs being printed, the size of each log statement, etc. We'll limit logging to development only. Hence, the store will only apply the redux-logger middleware during development. Note : You can configure redux-logger to log certain actions or collapse actions that didn't result in an error. Visit the redux-logger GitHub repository and read the documentation for more information. Only a single set of reducers will be added to the store: the counter reducer. It will modify the portion of the state tree that the <Counter /> component uses. ( shared/redux/reducers/index.ts ) Anytime an action of type INCREMENT or DECREMENT is dispatched, the counter reducer will either increment or decrement the stored count by one. Make sure that all operations involving state inside the reducer are immutable for time-travel debugging. ( shared/redux/reducers/counter.ts ) Define two action creators for the INCREMENT and DECREMENT actions: ( shared/redux/constants/index.ts ) ( shared/redux/actions/index.ts ) The <Counter /> component increments and decrements the value of count stored within the Redux store. Connect the component to the Redux store by calling Redux's connect function and passing it mapStateToProps and mapDispatchToProps . mapStateToProps determines what part of the data from the store should be passed to the component. In this case, the count value from state.counter will be passed to the component as the prop count . mapDispatchToProps determines the actions the component can dispatch. In this case, the component should be able to dispatch the INCREMENT and DECREMENT actions. The INCREMENT action will be dispatched when the component calls the onIncrement prop, and the DECREMENT action will be dispatched when the component calls the onDecrement prop. ( src/components/counter/index.ts ) Inside of the <Counter /> component, display the current count's value within a <Text /> component and assign the onIncrement and onDecrement functions to the "+ Increment" and "- Decrement" buttons' onPress respectively. Pressing on any of these buttons will either increment or decrement the count. For example, calling onIncrement dispatches the INCREMENT action, which gets handled by the counter reducer and updates count in the store to count + 1 . ( src/components/counter/Component.tsx ) Ignoring the src/components/counter/index.ts file, you could easily move this <Counter /> component to a different codebase that doesn't depend on Redux, and it would work perfectly fine as long as you provide the same set of props: Finally, wrap the part of the application that interacts with the Redux store within a <Provider /> component. ( App.tsx ) Try it out in the iOS simulator by running the following command: Notice how the redux-logger middleware, much like middleware being executed on every incoming request in Express.js , gets executed on every dispatched action. Redux Thunk serves as syntactic sugar for asynchronous actions. It eliminates the requirement of passing the dispatch function to the action creator by having the action creator return an anonymous function that automatically provides the dispatch function. To support thunks, add Redux Thunk to the Redux store's middleware: ( shared/redux/store/index.ts ) If we added a save button that sends a request to a remote API to store the current count inside of a database, then we would have to write the action creator for this functionality as a thunk. For the purposes of this tutorial, we will simulate an API request via a setTimeout . After 300 milliseconds have elapsed (approximately the average response time for an API endpoint), dispatch an action that switches an isSaved flag within the Redux store to true . ( shared/redux/thunk/index.ts ) ( shared/redux/actions/index.ts ) ( shared/redux/constants/index.ts ) ( shared/redux/reducers/counter.ts ) Whenever the user increments or decrements the current count, the isSaved flag resets to false because the count has changed. This new value hasn't been saved yet. Like onIncrement and onDecrement within mapDispatchToProps , onSave will follow the same syntax. The only exception is that onSave dispatches a thunk that dispatches a standard action after completing some asynchronous work. ( src/components/counter/index.ts ) Lastly, add the save button to the <Counter /> component and display status text that shows "Saved!" whenever the user presses the save button. ( src/components/counter/Component.tsx ) For a final version of this demo application, check out the source code at this GitHub repository . Reload the application and try out the new save button! Check out Amit Mangal 's React Native starter kit with TypeScript, Redux, Redux Thunk and React Native Navigation for a live, in-depth example on how to properly integrate Redux into a React Native project. Next time, try adding Redux and Redux Thunk into your next React Native project!