Create a React Native Web App with Hooks, Bells, and Whistles with Expo SDK33 [Part 2] — Linting and Redux

This is the second part of our tutorial series about React Native, Expo and the new features of Expo SDK 33. Today we’re gonna get our hands dirty with redux, and while we are at it, we’ll clean up a bit and set some presets for all future development using a .editorconfig and linting.

[Part 1] Setup and Navigation
[Part 2] ← You are here!

If you want to skip Part 1, feel free to grab the code from the v0.1.1 tag on GitHub.

Before we move on, I want to point out that there are very many different opinions on how code should look like and I won’t start a discussion about this topic. The choices I’m going to show you in this section are just one opinion amongst many on how to code and there are very religious groups out there that can educate you on the many little nuances of coding guidelines and best practices available.

Exchanging Ideas — Argument and Debate Concept (by Jack Moreh)

The .editorconfig file

If you are working in a team, it really helps to unify the way our code is written because we spend far more time reading code than we do writing it. Wouldn’t it be great if our IDE automatically did small changes while we work so A) we get a better feeling for the requirements on the go and B) those settings can be saved, versioned and shared across the team?
Yes, that would be great. I guess that’s why the people working for editorconfig.org started this awesome project and many IDEs automatically support a .editorconfig file from the getgo. If your IDE of choice happens to be missing on the list of IDEs with support for this file, there are compatible plugins for all major and even some minor alternatives.

You can find a copy of my .editorconfig on GitHub. I’m usually working with IntelliJ IDEA, which has full support out of the box but as I’m currently using VSCode for my tutorial projects, it took me less than 5 minutes to find and install a suitable plugin and include my presets in all my new projects.

Linting

Linting our code is a great way to make sure that all code checked in complies with our agreed-on styles and rules. The linter takes a set of rules and checks your code for compliance with those coding guidelines. We’re going to use ESLint to do that for us. First of all, let’s grab all required packages from npm and install them as development dependencies.

npm install --save-dev eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y babel-eslint

In addition to ESLint itself, we are going to use a well known and often adapted preset of rules developed by Airbnb. To make everything work with react and babel, we are going to need a few plugins as well. The usual way to implement linting in a project is to install the packages and then run the following command.

eslint --init // Don't run this line, if you use an existing config!

As I already have a battle-tested veteran .eslintrc file with my preferred config, I’ll skip that step and copy the existing file into the project.

To run the linter, we’ll add 2 lines to the scripts in our package.json between “web” and “eject”.

"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"lint": "eslint src/ --ext .js,.jsx || true",
"fix": "eslint --fix src/ --ext .js,.jsx || true",
"eject": "expo eject"
},

In those script we tell eslint to check the src/ folder for files with the extension .js and .jsx respectively.

I’ve added the “|| true” to fix (read: hack away) an ugly npm LIFECYCLE error output in the console, that bugged me. Use this with care and don’t do that, if you plan to chain commands where the exit code of the linting is important for following scripts. The “true” parameter will cause the script to exit with a 0 exit code (success!), even if there were errors while linting.

npm run lint => we’ve made some mistakes, as identified by the new rules, we’ve established

Running the linter shows us a list of files that include errors and warnings of code that’s not in line with our eslint rules. In this case, we can easily fix those errors with eslint using the “ — fix” flag included in our second script.

npm run fix

Now that the linter has fixed those minor errors, we are left with 3 identical warnings, informing us about the incorrect use of JSX code in .js files. To fix those, we have to do a bit more. If we change the extension of those files from .js to .jsx, the metro bundler that Expo uses won’t know how to deal with them. To fix this, we’ll add a small metro config section to our package.json file to tell metro that js isn’t the only filetype, it’s allowed to resolve.

"metro": {
"resolver": {
"sourceExts": [
"js",
"jsx",
"json"
]
}
},

Now we can safely change our file extensions to .jsx and restart the DevTools.

Changes to the config files won’t take effect until you restart the DevTools as those files are read only once on startup. Simply cancel the active expo process in the command line by pressing “ctrl + c” and restart them by typing npm start.

Redux

“A predictable state container for JavaScript apps.”

If you want to know more about redux and why to use it, I’d advise you to read the Motivation Chapter and following Section from their website. For the rest of this tutorial, I’ll assume that you are A) roughly aware, what Redux is and B) sure that you actually need Redux.

In order for Redux to work in our application, we need to include an additional library to bridge React and Redux and connect our application components to our store. We’ll include both in our regular dependencies via npm.

npm i -save redux react-redux

The basic idea behind Redux is to create a centralised store for all data produced and required by our app. We will trigger actions that request a change of the state when interpreted by a reducer and only read data from the store. No part of the app is allowed to change the Redux store but Redux itself. This way we have a single point of truth when our data is concerned and you can use an additional middleware layer for extra calculations and checks to keep actions and reducers as simple as possible.

Basic Setup

I’ll write and link to a more detailed Redux tutorial later. For now, I advise you to grab my redux/ folder from this GitHub Commit and continue from there. I’ll guide you through the process of creating a new action and reducer for our app in the following section.

After adding the redux folder and files to our src/ folder, the only thing left to do is to wrap our app in a provider that allows us to access the store. Let’s adjust our App.jsx file accordingly.

// App.jsx
// add those imports
import { Provider } from 'react-redux'
import configureStore from './src/redux/store'
// create the store object
// adjust our export with the <Provider> tag
const store = configureStore()
export default function App() {
return (
<Provider store={store}> // our new wrapper
<Navigation />
</Provider>
)
}

That’s it! From now on, we can easily dispatch actions and read the current state by hooking up our components with the react-redux connect function.

Basic Example

Let’s say that we want to monitor the current version of our app with redux and display it on the home screen. Taking note of the current app version makes a lot of sense as soon as you have persisted local data and want to check if the current version on load is higher than the last time the user opened the app. We’ll skip the persisted state for now but let’s add the current version to the home screen nonetheless and while we’re at it, we’ll add propTypes to our react component to make sure that our prop types match our definition. Quickly run the following line to install the respective npm package.

npm i -save prop-types

The changes to our Home Scene will be a bit more extensive but once you get used to this pattern, it practically writes itself. Let’s start by importing PropTypes and the connect function to our SceneHome.jsx.

// SceneHome.jsx
import PropTypes from 'prop-types'
import { connect } from 'react-redux'

The next thing we need to do is to establish the correct types for our props and map them to the redux state. To do so, we have to go through 3 simple steps, that you will repeat a lot of times while writing react and redux code.

  1. define the propTypes (TypeScript would be an alternative but that’s a whole different tutorial altogether)
  2. map them to the Redux state object
  3. connect the component to redux via connect() and pass in your mapped props
// SceneHome.jsx 
SceneHome.propTypes = {
applicationState: PropTypes.object.isRequired,
}

const mapStateToProps = state => ({
applicationState: state.application,
})

export default connect(
mapStateToProps,
)(SceneHome)

You can now access the props object in your component and for better readability, we’ll deconstruct them to access our applicationState and print our version as a Text element.

// SceneHome.jsx
const SceneHome = (props) => {
const { applicationState: { version } } = props
return (
<View style={styles.container}>
<Text>Hello World!</Text>
<Text>Home Page</Text>
<Text>
{`Version: ${version}`}
</Text>
</View>
)
}

Currently, we are not storing any useful information about the app. Go to the application reducer and add “version: -1” to the initial state to signal that the version hasn’t been set yet.

const initialState = { 
status: false,
version: -1,
}

Right now, the app will show our current version as -1 so let’s implement our first simple redux action. Create a file called application.actions.js under redux/actions and paste in the following code.

// application.action.js
export const INIT = ‘[INIT]’
export const INIT_APPLICATION = `${INIT} Set Initial values for the application`
export const initialiseApplication = () => ({
type: INIT_APPLICATION,
payload: {},
})

You could use a simple string instead of the INIT_APPLICATION constant but writing it this way makes a lot of sense if you plan to keep the app scalable and the redux log readable. Again, I’ll soon post a redux tutorial describing my practices as well as the reasoning behind them and edit the links in here for reference.

Having implemented the new action, we need to react to it in our reducer so let’s adjust our application.reducer.js accordingly. We’ll import the INIT_APPLICATION constant we’ve defined earlier and we will also import the manifest from our app.json to access the current name version number.

Then we tell redux to watch out for any action of our init type and return a new object for our state with the changed status (now true) as well as the version and name from the manifest.

// application.reducer.js
import { INIT_APPLICATION } from '../actions/application.actions'
import manifest from '../../../app.json'
const { expo: { name, version } } = manifestconst initialState = {
status: false,
version: -1,
}
const applicationReducer = (state = initialState, action) => {
switch (action.type) {
case INIT_APPLICATION: {
return {
status: true,
version,
name,
}
}
default: {
return state
}
}
}
export default applicationReducer

We want the app to initialise as soon as it’s ready and to do that, we will make use of the new useEffect hook.

Let's have a quick look at how the useEffect hook. It is comparable to the lifecycle methods of typical class-based react components for componentDidMount, componentDidUpdate and componentWillUnmount. The hook’s usage is pretty simple. You provide the useEffect hook with all props and functions you want to use and the code you put inside the hook’s body will be called one time as soon as the component is mounted and again every time the value of any of the parameters in the provided array changes.

// example 1 // not part of the tutorial codeuseEffect(() => {
checkInit()
}, [checkInit])

In this simple case, we only put in our checkInit function so it will run once just like componentDidMount and then never again.

// example 2 // not part of the tutorial codeuseEffect(() => {
console.log('init!')
return () => console.log('unmounting...')
}, [checkInit])

You can emulate componentWillUnmount by returning a function. This will be called by react prior to unmounting.

// example 3 // not part of the tutorial codeuseEffect(() => {
const newCalculatedOutput = calculate(someState)
console.log(newCalculatedOutput
}, [calculate, someState])

Let’s say, “someState” changes during the lifetime of our example component. Our useEffect hook will calculate our output once on mount and then again every time “someState” changes.

So after this short digression, let’s get back to our tutorial app and adjust our SceneHome.jsx file. We need to import out new action, map it to our props and call it in our useEffect hook. By doing so, the app will mount our component, the hook will dispatch our action to redux, our reducer will change the state in our store and our component will update because our props are matched to the state in the store. For bonus points, I also add the app name to the version number in the component's output.

// SceneHome.jsx
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { StyleSheet, Text, View } from 'react-native'
import { initialiseApplication } from '../redux/actions/application.actions'

const SceneHome = (props) => {
const { applicationState: { version, name }, checkInit } = props

useEffect(() => {
checkInit()
}, [checkInit])

return (
<View style={styles.container}>
<Text>
{`${name} v${version}`}
</Text>
<Text>Home Page</Text>
</View>
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
})

SceneHome.propTypes = {
applicationState: PropTypes.object.isRequired,
checkInit: PropTypes.func.isRequired,
}

const mapStateToProps = state => ({
applicationState: state.application,
})

const mapDispatchToProps = dispatch => ({
checkInit: () => dispatch(initialiseApplication()),
})

export default connect(
mapStateToProps,
mapDispatchToProps,
)(SceneHome)

If you check now, our current version is set to a stable major release “1.0.0” as that's usually the default if you start a new project. I’d say we reserve the first major release for the end of our tutorial series for when we finished our little project so let’s set a more realistic version number in our app.json file. I’m going with “0.1.2” as we are currently in the second part of our tutorial series and until we’ve laid out all the basics, I won’t increase the minor version to “0.2.0”.

Let’s Recap: We’ve added Redux to our project and made sure that our code is clean by employing a .editorconfig file as well as some linting. Our next steps will be a basic Redux implementation as well as some more useful examples for Navigation and our Redux Store.

If you want to take a look at the current code, go to the tag v0.1.2 — Base Redux Setup on GitHub. If you had any problems, following along, feel free to ask questions in the comments. I’ll try to get back to you as soon as possible.

Once this series is done, I’ll start writing a series on basic game design principles and how to build simple games for iOS, Android and the web, using Expo and React Native. If you want to support me in writing more articles like this or if there is a specific topic you would like to learn more about, feel free to visit my Patreon Site. I’ll post regular updates there as well as some special content and previews of projects I’m working on as a bonus for backers.

Next steps: In the next part of this series, we will start adding some value to our tutorial app by writing an ideas/buzzword generator and actually comparing the current version of our app with the one, the user had installed last time the app ran. We’ll dig deeper into redux middleware for this and start storing some data locally to make use of a persisted state.

Outlook: Once this series is done, I’ll start writing a series on basic game design principles and how to build simple games for iOS, Android and the web, using Expo and React Native. I’ve also started a series of straightforward tutorials for Redux and React will follow soon. If you want to support me in writing more articles like this or if there is a specific topic you would like to learn more about, feel free to visit my Patreon Site. I’ll post regular updates there as well as some special content and previews of projects I’m working on as a bonus for backers.

I’m a Web / App Developer & father 👨‍👩‍👧 doing freelance and part-time agency work since 2003, 💻 building stuff on the side 🕹 and attending conferences 🎟