Install Button in PWA React+Redux

June 10, 2019

3 min read

Install Button in PWA React+Redux

In this post, we will look at how to make a React+Redux app installable on both desktop and mobile phone.

Installing Increaser

Before we start adding React/Redux specific logic, we need to be sure that the app we are working on is have everything listed in this checklist. At the moment of writing, they are valid manifest and service worker and that your app served over HTTPS. You can check if your app installable by using audits in developer tools.

Checking if your app is installable

Action and Reducer

We need two actions — one to save the proposal event and second to remove it from the state. To create actions and reducers, we could use the redux-act library.

// actions/generic.js
import { createAction } from "redux-act"

export const saveInstallProposalEvent = createAction()
export const promptToAddToHomeScreen = createAction()

// reducers/generic.js
import { createReducer } from "redux-act"
import * as a from "../actions/generic"

const getDefaultState = () => ({
  proposalEvent: undefined,
})

export default () =>
  createReducer(
    {
      [a.saveInstallProposalEvent]: (state, proposalEvent) => ({
        ...state,
        proposalEvent,
      }),
      [a.promptToAddToHomeScreen]: (state) => ({
        ...state,
        proposalEvent: undefined,
      }),
    },
    getDefaultState()
  )

Root Component

In the root component, we attach event listener to the window at componentDidMount, when beforeinstallprompt event fires, we call saveInstallProposalEvent action to save the event in the state.

// components/layout.js

class Layout extends React.Component {
  componentDidMount() {
    window.addEventListener("beforeinstallprompt", (e) => {
      e.preventDefault()
      saveInstallProposalEvent(e)
    })
  }
}

Install Button

The last thing we need to make is to place somewhere button that will be rendered only if proposalEvent in the state.

// components/install.js
import React from "react"
import styled from "styled-components"

import { connectTo, takeFromState } from "../utils/generic"
import { promptToAddToHomeScreen } from "../actions/generic"
import Button from "./button-with-icon"

const Container = styled.div`
  margin: 10px;
`

const Component = ({ proposalEvent, promptToAddToHomeScreen }) => {
  if (!proposalEvent) return null
  const onClick = () => {
    promptToAddToHomeScreen()
    proposalEvent.prompt()
  }

  return (
    <Container>
      <Button
        onClick={onClick}
        centeredText
        text="Install Pomodoro"
        icon={"download"}
      />
    </Container>
  )
}

export default connectTo(
  (state) => takeFromState(state, "generic", ["proposalEvent"]),
  { promptToAddToHomeScreen },
  Component
)

Redux utility functions that were used in the example above you can find in the snippet below.

// utils/generic.js
import { connect } from "react-redux"
import { bindActionCreators } from "redux"

export const connectTo = (mapStateToProps, actions, Container) => {
  const mapDispatchToProps = (dispatch) => bindActionCreators(actions, dispatch)
  return connect(mapStateToProps, mapDispatchToProps)(Container)
}

export const takeFromState = (state, stateObjectName, fields) =>
  Object.entries(state[stateObjectName]).reduce(
    (acc, [key, value]) =>
      fields.includes(key) ? { ...acc, [key]: value } : acc,
    {}
  )