May Cause Side Effects: How to Implement Redux Sagas as Middleware

May Cause Side Effects: How to Implement Redux Sagas as Middleware

Written by Brenna Martenson and edited by Matt Rogers

First Things First: The What and The Why

redux-saga is a middleware library for Redux that handles asynchronous actions triggered in your application. Often referred to as 'side effects', these events are outside of the main flow of your application's funcationality, and in turn outside of your control. This makes side effects notoriously challenging to manage.

Often when building an appliation using Redux, you'll be guided to a library called redux-thunk to handle these events. In Redux world, actions that pass through the Redux library need to be in the form of a plain old javascript object. Async actions, however, tend to be more complicated and come in many shapes and sizes. redux-thunk allows you to send an action in alternative forms (think making a fetch request to some API).

This is incredibly useful, but thunk also handles a lot of the logic behind the scenes leading to what can feel like a magic middle-world that is hard to dissect, and hard to troubleshoot.

redux-saga, on the other hand, uses ES6 constructor functions to more explicitly break down each step of the process when an async action fires.

In this article, we will be working with a very simple application using React and Redux. In order to demonstrate the different use cases of these middleware libraries, we will first talk through existing functionality with redux-thunk, then we will refactor the application to use redux-saga.

The Plan

We'll be working on an application that looks something like this:

screenshot of app

This app fetches a daily image from the Nasa website as soon as the application loads, and displays it with its description on the page.

You'll need an API key

If you're coding along, take a minute to visit the Nasa Open API website and apply for your own API key - this should be sent to your email address momentarily.

Grab the starter repo

Grab a copy of this starter repo. This will contain the skeleton of the Redux app we want to start from (using create-react-app). At the end of this article you will also find a link to the completed repo for reference.

Once you've cloned it down, run npm install and npm start. You should see an error message referring to a missing API key. Go ahead and replace the API placeholder text with your new API key.

Getting Started

The Redux workflow revolves around the Redux store. As mentioned in The Plan, the goal of this baby application is simply to fetch an image from our API and display it on the page as soon as the application loads. Let's take a look at what currently exists in our application.



Index.js

In the primary src/index.js file, we already have our app set up to handle asynchronous actions using the popular library redux-thunk.

Here, we immediately dispatch an action to make an API call which will then modify state and re render the components that care.

    store.dispatch(getImage());

This line is associated with an action located in the actions/index.js file. It looks like this:

    // actions/index.js
    
    export const getImage = () => (
        fetch('https://api.nasa.gov/planetary/apod?api_key=YOUR-API-KEY-HERE')
        .then(response =>  response.json())
        .then(json => console.log(json))
        .catch(error => console.log(error))
    );

There are two main rules about actions in a flux based framework:

  1. Actions must be plain objects
  2. Actions must have a type

Right now our getImage action follows neither of those rules, and is intead the infamous "asynchronous action" we talk about to so often. Cue the Jaws theme song.

When we say asynchronous actions, we're referring to events that are triggered outside of the application's control. When these occur, your code is no longer explicitly in charge of when it gets information back, or when additional functions will fire. One or both of those events happen outside of the primary flow of the program.

Redux is designed to fire off synchronous actions - events that are controlled by the user. The moment an action is dispatched, Redux is trying to immediately update state and pass that new information to any components that need it.

Making an API call is an action that requires additional stages to complete, is mostly out of the user's control, and needs to be handled differently. Redux can't treat it like a simple action object because at the most basic level, it's NOT a simple object.

From the docs, there are three primary stages we need to address when trying to handle these situations:

  1. When the request was initiated
  2. We should tell our reducers that the request has started. This lets us implement a "spinner" or "loading" feature in case this takes a while.
  3. When the request finished
  4. We need to update our reducer when the request is complete, and this period of time is out of our control.
  • We can then stop the spinner and display the data received
    1. If something goes wrong
    2. You may want to display error messages to the user, in which case our reducers need to be told how to handle any unsuccessful requests and what to tell the user.

Thunk To The Rescue!

Think of middleware libraries like big nets in the ocean of your application, each in charge of watching for a particular bit of code to float through. These libraries, like redux-thunk, intercept a type of action that doesn't fit the "plain object" mold, and carry out whatever side jobs need to happen before it can be passed on. This happens outside of the normal flow of your application.

Back in src/index.js you'll notice that thunk gets passed in when we create our store through the Redux based method applyMiddleware().

    // src/index.js
    
    import thunk from 'redux-thunk';
    
    // Pop thunk into our applyMiddleware method
    const store = createStore(
      rootReducer,
      composeWithDevTools(
        applyMiddleware(thunk)
      ),
    );

Let's take a quick look at what testing async actions with redux-thunk might look like.

Testing with Redux Thunk

Because redux-thunk has a lot of behind the scenes magic, we need to call in the helper libraries fetch-mock and redux-mock-store to mock out this functionality. You'll find this logic in src/__tests__/asyncActions.test.js. We also need an afterEach method to verify that we're starting from a clean slate after each test runs.

    // src/__tests__/asyncActions.test.js
    
    import fetchMock from 'fetch-mock';
    import configureMockStore from 'redux-mock-store';
    import thunk from 'redux-thunk';
    
    const middleware = [ thunk ];
    const mockStore = configureMockStore(middleware);
    
    import * as actions from '../actions'
    
    describe('getImage', () => {
    
      afterEach(() => {
        expect(fetchMock.calls().unmatched).toEqual([]);
        fetchMock.restore();
      });
    
      it('dispatches REQUEST_IMAGE action when getImage is fired', () => {
        fetchMock.get('*', {
          status: 200,
        });
    
        const store = mockStore({
          loading: false,
          data: {
            explanation: '',
            hdurl: '',
            title: ''
          },
          error: null
        })
    
        const expectedActions = [
          { type: 'REQUEST_IMAGE' }
        ]
    
        store.dispatch(actions.getImage())
    
        expect(store.getActions()).toEqual(expectedActions)
        expect(fetchMock.called()).toEqual(true);
      });
    
    });

The starter repo includes additional tests to demonstrate testing async actions with redux-thunk. Let's switch gears and refactor to use redux-saga.

Implementing Redux Saga

Redux-Saga allows you to organize all of the side effects that need to happen in your app in a single, completely separate thread. It can be accessed by both your main application and connect with the Redux store or dispatch actions.

The cool part about this library is that it implements ES6 Generator functions to write super readable, testable, "OMG WHERE HAS THIS BEEN" lines of code.

Let's look back at the action creator that we needed redux-thunk to intercept :

    export const getImage = () => (
      dispatch => {
        dispatch(requestImage());
        return fetch('https://api.nasa.gov/planetary/apod?api_key=YOUR_API_KEY_HERE')
        .then(response => response.json())
        .then(json => {
          if (!json.error) {
            dispatch(setImage(json))
          } else {
            // eslint-disable-next-line
            throw {message: json.error.message, code: json.error.code}
          }
        })
        .catch(error => dispatch(catchError(error)))
      }
    );

In this function we are trying to accomplish a variety of different things:

  1. Dispatch an action to tell Redux that we have started our request (to trigger a loader/spinner)
  2. Send out the fetch request
  3. Check to see if we receive a valid response
  4. If yes: Dispatch an action to set the image
  5. If no: Dispatch an action to do something with the error

That's a lot for one function. No part of that is single responsibility and it feels pretty gross.

Let's have a refactor party to swap out redux-thunk with redux-saga and play around with what this library offers.

Saga Setup

First things first, grab the library.

    npm i -S redux-saga

Next, let's create a file where all of our "sagas" will be kept.

    touch src/sagas.js

In this file we will put all of the side-effects that we expect our application to fire off. One immediate benefit to this is it keeps all of the messy stuff in one centralized location.

Before we start implementing this new code we need to refactor our main src/index.js file. We need to tell our store to grab the redux-saga library when it's first created, instead of redux-thunk. This will break a few things, but hang tight.

    // src/index.js
    
    import React from 'react';
    import { render } from 'react-dom';
    import { Provider } from 'react-redux';
    import { createStore, applyMiddleware } from 'redux';
    import { composeWithDevTools } from 'redux-devtools-extension';
    
    // NEW CODE: You can also remove the redux-thunk dependency
    import createSagaMiddleware from 'redux-saga';
    import sagas from './sagas';
    
    import App from './components/App';
    import registerServiceWorker from './registerServiceWorker';
    import rootReducer from './reducers';
    import './styles.css';
    
    import { getImage } from './actions';
    
    // NEW CODE
    const sagaMiddleware = createSagaMiddleware();
    
    //NEW CODE: Replace thunk with sagaMiddleware
    const store = createStore(
      rootReducer,
      composeWithDevTools(
        applyMiddleware(sagaMiddleware)
      ),
    );
    
    // NEW CODE
    sagaMiddleware.run(sagas);
    
    // END OF NEW CODE ADDITIONS
    store.dispatch(getImage());
    
    /// EXISTING CODE HERE

Let's talk about what some of these lines are doing.

Here we're importing the necessary createSagaMiddleware method from theredux-saga library. Then, we grab the newly created (currently empty) sagas.js file:

    import createSagaMiddleware from 'redux-saga';
    import sagas from './sagas';

Next, we delcare a named function to allow us to execute the createSagaMiddleware function:

    const sagaMiddleware = createSagaMiddleware();

Then, we replace thunk with sagaMiddleware when initializing our Redux store:

    const store = createStore(
      rootReducer,
      composeWithDevTools(
        applyMiddleware(sagaMiddleware)
      ),
    );

Finally, we tell saga middleware to run a particular list of sagas which will soon live in our sagas.js file:

    sagaMiddleware.run(sagas);

If you take a look at your browser now, you'll see we get another (relatively) helpful error message:

saga-error

This makes sense. Sagas are simply a series of generator functions, each with a list of yield expressions. Each expression is in charge of running one piece of the side-effect puzzle. We currently have nothing in our sagas.js file so it's telling us it needs more information.

Let's throw in a simple generator function into our sagas to get started.

    // src/sagas.js
    
    export default function* testSaga() {
      console.log('Wired up!');
      yield 'WIRED UP!'
    }

If you take a look at your console now, you might expect to see our log statement. Instead, we're back to a very common error message:

action-error

This is because we fire off the line that says sagaMiddleware.run(sagas);, and then we immediately dispatch our async action, but we've removed the library (redux-thunk) that worked the secret magic to intercept this function. This might seem problematic, but before we troubleshoot let's take a detour into ES6 generator functions.

ES6 Generators

As previously mentioned, sagas are written using the new, hip, generator function that is baked into ES6. It looks like this:

    function* doSomething() {
      yield 'You are a spiketail...'
      yield '...so we will call you Spike!'
    }

There are a few things to note here.

First, generator functions are indicated with an asterisk. (Note: The asterisk can be next to either the keyword function* or the function name *doSomething and the function will still be recognized as a generator.)

Second, the special content within the function starts with a "yield expression", the keyword yield. This is what tells the generator to pause.

Third, once the generator function is called, it will only execute code up until it encounters the keyword yield. This tells the generator function to return whatever is to the right of the yield.

Finally, after executing the code to the right of the first yield, it will wait to continue until you tell it to fire the next line, which is done using a Generator Iterator.

TRY IT OUT

Grab that function doSomething() mentioned above and throw it in a repl or your console.

What happens if you run doSomething()?

Hopefully, nothing. (If you're in the browser console you might see an output like Generator status suspended, but nothing actually gets executed). This is because this ISN'T a normal function and it can't be executed like a normal function.

In order to properly execute this function we need to iterate over the generator, by constructing a generator iterator.

Developers are the BEST at naming things.

    function* doSomething() {
      yield 'You are a spiketail...'
      yield '...so we will call you Spike!'
    }
    
    // This is the generator iterator!
    var gen = doSomething();

Then, to tell the generator iterator to DO something you need to run the method .next().

Before you run the next code snippet, take a guess as to what you expect to see.

    function* doSomething() {
      yield 'You are a spiketail...'
      yield '...so we will call you Spike!'
    }
    
    var gen = doSomething();
    
    gen.next();

The answer is super interesting! You might expect the return value to be the string 'You are a spiketail...' but instead the Generator returns a Generator Object.

You should see something print like:

    Object {value: 'You are a spiketail...', done: false}

Instead of simply getting back the string of our first yield expression, we get an object with two keys - the value of our iteration (You are a spiketail...), and a done boolean indicating if our function has finished executing (false).

And now it waits.

Continue to run a couple more gen.next() functions and see what happens - when does the done boolean return true?

You'll notice after each execution the code executes whatever is to the right of the next yield expression, and then just HANGS OUT.

This is the epitome of why generators are so helpful when we are dealing with async code. Typically we have very little control over what happens in between when something sends out an API call and when it moves on with that returned information.

As we saw in our redux-thunk example earlier, all we know is that we fire off a fetch call, then get some sort of response back. It either works or blows up, and often times we end up in a series of callbacks that can be unpleasant. What if we could step into that series of events and do more?

2 Way Communication With Generators

Another cool feature of generators, and one that we will leverage in sagas, is the ability to pass information back INTO a function in the middle of its execution.

    function *adding(){
      var result = 1 + 1
      return 20 + (yield result)
    }
    
    var sum = adding()

In the example above, we start with a generator function called *adding() that assigns a variable to a mathematical addition operation, and then returns a value that includes a yield expression.

To run this code, execute adding.next() in your console a couple times.

If you run the code exactly as listed above, you should see something like:

    Object {value: 2, done: false}
    Object {value: NaN, done: true}

First Iteration: We get a value of 2. Makes sense - if it stops at yield and fires whatever is to the right, we're asking for the variable result which is 2.

Second Iteration: It wipes out the yield expression and then replaces it with whatever argument we pass in. We are telling it to execute the line that says 20 + (the yield expression we just wiped out) which, naturally, is NaN, if we passed in nothing.

Try it again, but this time pass in a value the second time you call sum.next().

    function *adding(){
      var result = 1 + 1
      return 20 + (yield result)
    }
    
    var sum = adding()
    sum.next()
    sum.next(10)
    
    // => Object {value: 30, done: true}

We've just accomplished real life, two way communication with a LIVE FUNCTION. You can dig deeper here, but lets circle back to our original topic.

Now that these funky looking functions make more sense, let's go back to setting up our sagas.js file.

Implementing Sagas

Recall that in our main src/index.js file we have the following line:

    store.dispatch(getImage());

This line tells our Redux store to dispatch an action which currently lives in our actions/index.js file. If we hop over to that file, recall that this function looks like this:

    // actions/index.js
    
    export const getImage = () => (
      dispatch => {
        dispatch(requestImage());
        return fetch('https://api.nasa.gov/planetary/apod?api_key=YOUR-API-KEY-HERE')
        .then(response => response.json())
        .then(json => {
          if (!json.error) {
            dispatch(setImage(json))
          } else {
            // eslint-disable-next-line
            throw {message: json.error.message, code: json.error.code}
          }
        })
        .catch(error => dispatch(catchError(error)))
      }
    );

We'll reference that function in a bit, so comment it out for reference and replace it with:

    export const getImage = () => ({
      type: 'INITIALIZE_IMAGE_SAGA',
    });

Now our dispatch method fires off a plain action object, instead of the multi-job async fetch call. FINALLY we can set up a Saga to step in and handle each intermediate step line by line.

If you check your console now, you'll see our dummy console log since we've effectively commented out all of the functionality that does anything important.

To change this, let's get into redux-saga effects.

Saga Effects

These effects are part of the redux-saga library and are standing guard, waiting to be put to use when a particular Redux action is triggered.

A few we will cover are:

  • takeEvery - 'be on guard for a specific action'
  • put - 'dispatch an action to the Redux store'
  • call - 'make some sort of API call'

To start rebuilding our functionality we need to add a function that uses the Saga effect takeEvery. This effect is the most similar to redux-thunk. It's job is to stand guard and listen for whatever action has been dispatched, intercepting that action if it matches the specified type. (Think back to the sea net analogy). This is our net.

When it intercepts that action type, it fires off the callback function passed in as its second argument.

    export function* watchGetImage() {
      yield takeEvery('INITIALIZE_IMAGE_SAGA', getImageAsync);
    }

In our case, when our app first spins up we tell the store to dispatch the action creator getImage(), which dispatches an action with the type of INITIALIZE_IMAGE_SAGA. We've modified our actions file to start this process, now let's update our sagas to start paying attention.

    // sagas.js
    
    // Grab a selection of saga effects
    import { call, put, takeEvery, all } from 'redux-saga/effects';
    
    // Remove the keyword 'default' here
    export function* testSaga() {
      console.log('Wired up!');
      yield 'WIRED UP!'
    }
    
    // Similar to events in event listeners, we get the ACTION 
    // that was fired for free as an argument that is passed 
    // to our callback.
    export function* getImageAsync(action) {
      console.log(action);
    }
    
    // This function will now watch for any time it 
    // intercepts a `GET_IMAGE` action type.
    export function* watchGetImage() {
      yield takeEvery('INITIALIZE_IMAGE_SAGA', getImageAsync);
    }
    
    // Similar to having a root Reducer, we need one master 
    // Saga to export what we want to run when our store is created
    export default function* rootSaga() {
      yield all([
        testSaga(),
        watchGetImage(),
      ]);
    }

A quick breakdown of the logic above. The most important function in here is the new function attached to our export default line (note we've removed export default from testSaga). Here, I called it rootSaga(). This is what our primary src/index.js line will run when it hits the line sagaMiddleware.run(sagas). This will send whatever we are exporting in our sagas file through the Redux middleware.

In this case we are telling it it to run a list of sagas indicated by the effect all. First, testSaga will fire (simply keeping our console.log for reference), then it hits watchGetImage.

    export function* watchGetImage() {
      yield takeEvery('INITIALIZE_IMAGE_SAGA', getImageAsync);
    }

Here, watchGetImage() leverages the saga effect takeEvery. We've told it to be on the lookout for an action with the type 'INITIALIZE_IMAGE_SAGA'. If/when that happens, fire the getImageAsync function.

Next, the getImageAsync will fire, receiving an action object for free. For now we've simply told our app to log that action.

More on Saga Effects

redux-saga provides us with a few different "effects" and helper methods that scoot things along. We just used takeEvery, which "takes" "every" action that matches a pattern it's been given and fires off the next saga when that pattern is matched.

Let's look at two more:

put

  • Example: yield put(requestImage())
  • Tells the middleware to dispatch an action to the Redux store.
  • Returns an object of instructions
    {
      value: {
      PUT: {
        action: {
          type: 'REQUEST_IMAGE', channel: null
        }
      }
    }
    done: false
    }

call

  • Example: yield call(fetchImage, ...args)
  • Used when we need to get data asynchronously, and might need to do some stuff in between (like when you use fetch to make an API call)
  • Takes two arguments - a callback, and an optional spread of additional arguments
  • Returns a plain object describing what instructions it is following out.
    {
      value: {
      CALL: {
        args: [
          0: 'first argument',
          1: 'second argument',
        ],
        context: null,
        fn: function fetchImage()
        }
      }
    }
    done: false
    }

There are many more effects and helpers we can use, some of which are blocking (meaning they will wait for whatever they've been told to do to resolve), some are non-blocking (meaning they wait for no one). Check out this section of the docs for all the goods.

Restructuring our API call

So now let's do something more interesting with the getImageAsync function we fired off. In this function, we want to fire off each of the action creators from the monster function we commented out in actions/index.js.

Recalling the objectives of our application, we still want to:

  1. Tell Redux we are requesting an image, so do the Loader thing
  2. Fetch the image in the meantime
  3. Store the image in state if things go well
  4. Figure out what to do if stuff breaks

Import the following additional resources into your sagas.js file:

    // Pull in our action creators
    import { catchError, requestImage, setImage } from './actions'
    // Grab our fetch function (that we haven't made yet)
    import { fetchImage } from './fetch'

And update the getImageAsync generator function:

    export function* getImageAsync(action) {
        console.log(action)
    
        // Dispatch the 'REQUEST_IMAGE' action to the store 
        // to kick off our loader
        yield put(requestImage())
    
        // Make our async call to fetch
        const data = yield call(fetchImage);
    
        // If we're getting back what we want
        if (data && !data.error) {
    
          // Dispatch the 'SET_IMAGE' data to the store to 
          // do something with our API results
          yield put(setImage(data))
    
        } else {
    
          // Dispatch the 'ERROR' action to the store 
          // to tell the user something went wrong
          yield put(catchError(data))
        }
      }

You should now see an error about our missing fetch resource. The beauty of using sagas is it lets you isolate a lot of your functionality into smaller, more testable pieces. Let's create a file who's sole job is to make our fetch call.

    touch src/fetch.js

    // fetch.js
    
    const handleErrors = (json) => {
        if (json.error) {
          throw { message: json.error.message, code: json.error.code}
        }
        return json;
    }
    
    export const fetchImage = () => (
      fetch('https://api.nasa.gov/planetary/apod?api_key=YOUR-API-KEY-HERE')
      .then(response =>  response.json())
      .then(json => handleErrors(json))
      .catch(error => error)
    );

At this point, your app should be back to full functionality.

Throw a few console logs at the end of your sagas file to watch how the saga progresses:

    // src/sagas.js
    
    const gen = getImageAsync();
    console.log(gen.next());
    console.log(gen.next());
    console.log(gen.next());
    console.log(gen.next());

Testing Sagas

We previously looked at testing redux-thunk, so now lets dig into how to test sagas.

The only cumbersome bit is grabbing all of the things associated with our sagas. The flip side is that we are now dealing with ES6 Generator functions. This means we can run each line of code and test each specific yield expression to ensure that we are getting back that plain old JS object with the value we expect.

This also means we no longer have to mock out a magical fetch call because at the end of the day we aren't testing that fetch behaves correctly - we're testing that our application behaves correctly given a particular output from fetch.

Let's start with our first simple generator example *testSaga() to see what this looks like.

    // __tests__/sagas.test.js
    
    import { takeEvery } from 'redux-saga';
    import { call, put, take } from 'redux-saga/effects';
    
    import { requestImage, setImage, catchError } from '../actions';
    import { fetchImage } from '../fetch';
    
    import { testSaga, getImageAsync, watchGetImage } from '../sagas';
    
    describe('test saga', () => {
    
      it('calls the test saga function', () => {
        const generator = testSaga();
        const testValue = generator.next().value;
    
        expect(testValue).toEqual('WIRED UP!');
      });
    });

This function only has one yield expression, so we only need to iterate over it once, and assert that we get the value back we expect once.

Let's add another describe block for the getImageAsync() generator to see if it gets more complicated.

    describe('getImageAsync', () => {
      it('handles a successful function', () => {
        const generator = getImageAsync();
    
        const data = { hdurl: 'some url', explanation: 'stuff and things', title: 'words', }
    
        expect(generator.next().value).toEqual(put(requestImage()), 'makes a dispatch to request image ');
        expect(generator.next().value).toEqual(call(fetchImage), 'makes a call to fetch the image');
        expect(generator.next().value).toEqual(put(setImage(data)), 'dispatches set image')
      });
    });

WHAAAAT. It failed!

That was on purpose.

Look at that third assertion - remember that if things go well, setImage(data) needs data! (If you look closely at the failed test you'll notice it's triggering our ERROR action...so technically it's following orders.)

The setImage(data) gets that data from the result of the previous yield expression. Think back to that concept of 2 way communication within generators - right now we are in a fake testing environment, we have to manually run each generator.next().value line, the saga library isn't doing that for us. But this gives us lots of control.

Now we can just tell our generator what's up. Like 'hey - let's pretend this is what you got back after that last yield expression. Carry on and make sure the rest of the instructions make sense.'. We're essentially stubbing in whatever we want to get back from our API call, and letting our generator move along.

    // Pass data into our generator iterator when we need it to be accessible
      expect(generator.next(data).value).toEqual(put(setImage(data)), 'dispatches set image')

Finally, let's add a sad path to make sure that if our try/catch block fails that we trigger the error handling.

    it('handles an unsuccessful function', () => {
      const generator = getImageAsync();
    
      const error = {
        code: 'BROKE STUFF'
      }
    
      expect(generator.next().value).toEqual(put(requestImage()), 'makes a dispatch to request image ');
      expect(generator.next().value).toEqual(call(fetchImage), 'makes a call to fetch the image');
      expect(generator.next(error).value).toEqual(put(catchError(error)), 'catches the error')
    });

Final Thoughts

You should now have a very small, but relatively tested and functional react/redux app usingredux-saga. At this point, the functions that live in the actions file are all plain, manageable, javascript objects that make Redux happy. All of the complicated logic has been ported over to our sagas file, each of which also return simple javascript objects. Our fetch call is isolated to live on its own and can be reused if needed.

As always, there are many opinions and reasons to implement one particular library over another. This article aims to demonstrate implementation of a small slice of the very large JavaScript async pie.

Cheers!

References

Completed Repo Code

Iterators and Generators Docs

Redux Saga Effects

Redux Docs

Redux Saga Docs

Writing JS apps for Fitbit Ionic

Writing JS apps for Fitbit Ionic

This is Fine: How I Learned to Love the Dumpster Fire

This is Fine: How I Learned to Love the Dumpster Fire