Selectors

Selectors are not technically part of Redux itself. But the idea of using selectors is "officially endorsed," and you'll see that we already wrote some selectors in the last chapter.

A selector is simply this: A function that takes the current application state and returns the relevant portion needed by the view.

Now you may also start to see why I insisted on naming the mapStateToProps function select in the last chapter. It is because that select function is, in fact, a selector. To help jog your memory, it looks something like this:

const select = appState => ({
  results: appState.results,
  query: appState.query
})

In that example, there isn't much logic. But selectors can do a whole lot more, and in my opinion, they can be one of the most exciting/powerful patterns in a Redux app.

What are they for exactly?

Selectors let us ask questions of our state. If we think of our application state as a database, we could say that if actions cause "writes" to the database, selectors are how we do "reads" from the database.

In the example above, it really is a straight "read." It's merely grabbing a couple of values unaltered from application state and just handing them to the component. But, let's use a simple example to show how we often need to ask much more interesting questions of our state.

Let's say we get 300 results back from our image search API call, and we don't want to show them all on the page at once. We may write a selector that tells us how many pages of results we have.

We could write a selector that does that, right? Perhaps something like this:

// note it takes the entire app state an argument
const selectPagesOfResults = appState => {
  const { results } = appState
  if (!results || !results.length) {
    return 0
  }
  // let's say we want 20 results per page
  return Math.ceil(results.length / 20)
}

Now we could take that selector function and just import and use it directly when writing the select function for a certain component:

import { connect } from 'react-redux'
import { selectPagesOfResults } from './our-selectors-somewhere'

const MyComponent = () => (
  ...
)

// our select function
const select = appState => {
  // now we can use the selector we wrote
  // and imported here!
  pagesOfResults: selectPagesOfResults(appState),
  results: appState.results
}

export default connect(select)(MyComponent)

If you're paying attention, you'll probably start to see where this is going. First of all, the way it's written above makes no sense because it's still returning all the results. Also, we just hard-coded that there should be 20 results per page into the body of our selector function, but what if that's a dynamic value that a user can set or that we want to be able to extract into a config file somewhere?

Really, if we're going to introduce pagination of the results we have already fetched we'll need answer quite a few questions:

  1. How many total pages of results do we have?
  2. What page number is the user viewing currently?
  3. How many items should be on each page?
  4. Based on all that information, what results should we be showing right now?
  5. Based on the current page, the number of results, etc. Should we show controls for going to the next page, previous page, or both?

This is the point at which most folks new to Redux tend to make a mess

A common mistake is to try to calculate and store this derived data as part of the application state. Believe me when I tell you: that is a mistake!

An excellent way to tell if you're doing this is if you're finding yourself writing a reducer that updates a derived value as part of handling a specific action. It's pretty safe to say you should probably be using a selector to read that derived value instead.

Interestingly, this idea of derived values is where we start to wade into what would be considered Functional Reactive Programming or FRP. The basic gist of which is, in a word: "spreadsheets." If you think about how a spreadsheet works, it's based entirely on the concept of describing relationships between "cells" in your spreadsheet. If you want to analyze a mortgage loan, for example, you have a few inputs: loan amount, years of the loan, and the interest rate that you can use to extrapolate entire tables full of derived information. You can, for example, derive an amortization table that shows how much each future payment contributes toward the principal or interest.

This is an extremely powerful programming paradigm. Once you've described how all the spreadsheet cells are related to one another, you can derive a considerable amount of valuable information just by changing the inputs.

I'd like to emphasize that this the exact same thing we're going to do with selectors in Redux. To prove the point I built a mortgage calculator with Redux as one of the included examples. You can see it here: https://reduxbook.com/mortgage

Before we go too far, however, let's write some selectors for the pagination example.

First, there are two additional pieces of data we need to track in our application state:

  1. currentPage the index of the page of results we're viewing.
  2. resultsPerPage to support changing this value dynamically; we can track this in our store as well.

For simplicity, let's extend our initial state a bit:

state = {
  query: 'puppies',
  fetching: false,
  results: [],
  // new properties:
  resultsPerPage: 20,
  currentPage: 0
}

Now, let's write a selector that grabs the results we should be showing on the current page.

export const selectCurrentResults = state => {
  // grab a few values we're going to need and store them
  // as variables.
  const { results, currentPage, resultsPerPage, fetching } = state
  // let's get the length too, to keep our function
  // more readable
  const { length } = results

  // if we don't have any results or we're
  // currently fetching some, stop here
  if (!length || fetching) {
    return null
  }

  // Now, we need to figure out where we are at and
  // return the right subset of results.
  // Array.prototype.slice works well for this, we just
  // need our start index and end index.
  const startIndex = resultsPerPage * currentPage
  const endIndex = startIndex + resultsPerPage

  return results.slice(startIndex, endIndex)
}

See how that works? You now have a function that when called with the application state, returns the results to display on the current page. Now, instead of doing that logic inside a component that is supposed to display these results, we now have that logic extracted into a separate little function.

It's also worth noting that since this is a simple, pure function, it's also very easy to write a unit test for it. It's just a function; there's nothing to stub out, no component to "shallow render" or anything. For something as simple as this, you may argue it isn't worth writing a unit test at all. But the point is since there's no browser involved, and you're just testing functions and plain objects, it's straightforward to thoroughly test your logic in a set of unit tests that run in node.js from the command line, without needing to mess with browsers at all.

If you haven't figured this out yet, I'm a huge fan of selectors! We haven't even gotten to the good stuff yet!

Composing selectors

Remember the spreadsheet cells? When you build a spreadsheet that uses formulas to extrapolate or derive data that's neat. But, the real power comes from the fact that you can derive additional data from the derived data! It's data inception! You can reference a cell that either has a raw value or contains a function that derives data. Our other cell doesn't care whether the cell it's referencing contains a value or a function, it's just going to get the value (or result of the function) as its starting value. This lets us isolate our formulas or logic and to derive increasingly complex answers from our data. We could, with just a handful of input fields, using our mortgage example, ultimately derive an answer to the question: "can we afford this house?".

This concept of deriving answers from other derived answers is the definition of another potentially intimidating phrase you may have heard before: "function composition."

We can (and in my opinion should) isolate and compose our selector logic in the same way. If we don't, as we start to write more and more selectors for everything we'll soon find that we're repeating ourselves a lot. It'd be like using a spreadsheet but never being allowed to reference any cells that contained formulas. Sure, you could produce the same spreadsheet by copying/pasting the formulas in every place you wanted that derived value, but that would make things a lot harder. If you later realized that you made a mistake in the formula you'd also have to fix it everywhere you had copied it. It's so much easier to keep things clean, organized, and error-free when we compose selectors instead.

One option would be to throw your app away and just use a spreadsheet instead! Many apps would probably be simpler if they were just a spreadsheet, but, I digress. So how do we do apply this same concept to a Redux application? In our example state, we only have one reducer right now, which isn't realistic at all for a real app. In a real app, the very first thing we'd need to do is grab the relevant "slice" of our state.

A naïve approach may be to do something like this:

// let's assume our application state has a few more reducers and
// looks something more like this:
const applicationState = {
  // Instead of being the *entire*
  // state our image search handling
  // would probably live in it's own
  // reducer alongside other state
  // our application cares about.
  imageSearch: {
    query: 'puppies',
    fetching: false,
    results: [],
    // new properties:
    resultsPerPage: 20,
    currentPage: 0
  },
  // other reducers would handle other
  // portions of our state
  currentUser: { ... },
  networkStatus: { ... },
  errorReporting: { ... }
}

Now, the very first thing any selector that cared about image search would have to do is extract the right piece of data:

// we could extract this little "state read" as it's own
// little function:
const selectSearchState = state => state.imageSearch

// now we can compose that manually into our selector from above
export const selectCurrentResults = applicationState => {
  // remember now this is our entire application state
  // so we'd have to start by extracting the imageSearch
  // portion of the state
  const relevantState = selectSearchState(applicationState)

  // now we could continue the rest of the function as before
  ...
}

But wow, this is starting to seem somewhat laborious, right? If we're going to be writing a lot of selectors that depend on one another, you're going to be manually calling a lot of functions inside one another. What happened to the simplicity of referencing another cell in a spreadsheet?!

Also, remember how, in previous chapters, we talked about how we can skip a lot of the "deep comparisons" because we're using the immutability rule: If you change it, replace it. None of what we're doing here seems particularly efficient from a performance perspective. If we're going to hand the entire application state to each selector and run each selector each time any action is dispatched that doesn't sound ideal. If we've "connected" this selector to a component, it will run after every change to the application state, whether or not the component cares about it. So in addition to running the selector every time, we'd also shallow-compare the result of the select function we passed to connect(), which will always be different.

As it turns out, there are better patterns we can use.

Introducing "reselect"

Reselect is a tiny library for composing selector functions, designed to work with Redux's immutability approach. It's not officially part of Redux but its use is encouraged in the official Redux documentation.

Reselect lets us pass any number of other selectors as input functions that will pass their results to what we'll call our "result function." Remember this terminology. When creating a selector using Reselect, you'll always provide one or more input functions but will only write one result function.

Reselect lets us do this:

import { createSelector } from 'reselect'

const someSelector = createSelector(
  // These input functions can be other selectors
  // created by reselect or they can just be
  // plain functions.
  inputSelector1,
  inputSelector2,
  inputSelector3,
  // note that the results of the input functions
  // just get passed as arguments to the last function
  // in the order their listed above
  (result1, result2, result3) => {
    // do our logic here
  }
)

Now it's finally starting to look more like spreadsheet cell references! The input functions can either be direct cell references like: state => state.imageSearch or they can reference other selectors created with reselect. So now, we can isolate and compose logic however we see fit. This is an immensely powerful tool in the battle against complexity in your codebase. One of my most quoted tweets has been pinned on my profile for years:

If you don’t actively fight for simplicity in software, complexity will win. …and it will suck.

Reselect is one of the tools that really lets us push back against the risk of ever-growing complexity in a JavaScript application. Rather than having to write increasingly complicated functions that account for an increasing number of variables, we can divide our logic into smaller composable chunks of logic that depend on one another. Very importantly this happens in clean, isolated functions, not mixed into your components or rendering logic.

I can tell you from experience, time and time again that this is one of those little gems that can provide substantial gains in software quality. It will give you the confidence to observe and react to increasingly complex application states enabling you to build features that you would otherwise be hesitant to tackle at all. And, you can do it while keeping your code manageable, very easily testable, and far more maintainable.

A key thing to understand is that reselect assumes you're following the Redux immutability rule: If you change it, replace it.

So, in addition to making your code less repetitive (a.k.a. more "DRY") the selector functions created by reselect are far more efficient. This is because reselect will only call the result function (your final function) if any of the input functions return something different than the last time it ran. If the inputs are all the same, it will short-circuit and instead return the result that it computed and stored the previous time.

Let's see a simple example, first without reselect, then with reselect:

Without reselect:

import { createSelector } from 'reselect'


// we still need a simple function that just grabs
// the relevant slice of state as a starting point
const selectSearchState = state => state.imageSearch

// but now, we can use the above function as an input
// to a function as follows:
export const selectCurrentResults = (state) => {
  const searchState = selectSearchState(state)
  // now our `searchState` here is
  // *just* the relevant portion
  // of our state.
  // ... we'd continue our logic here
)

Now with reselect:

import { createSelector } from 'reselect'

// we still need a simple function that just grabs
// the relevant slice of state as a starting point
const selectSearchState = state => state.imageSearch

// but now, we can use the above function as an input
// to a function as follows:
export const selectCurrentResults = createSelector(
  // we pass simple function as an input
  // to the selector we're writing now
  selectSearchState,
  // the result of that "input function" gets passed
  // as an argument to the last function
  searchState => {
    // now our `searchState` here is
    // *just* the relevant portion
    // of our state.
  }
)

It's essential to understand that what createSelector returns is still just a selector function that takes the entire application state as an argument and returns the result of the last function. It's just a more efficient function.

In the above example, we're only passing one selector function as an input. But really, we can pass as many as we want.

Let's apply this pattern to our search pagination scenario and create a set of selectors that tell us what we need to know:

import { createSelector } from 'reselect'

// simple input function that grabs the right slice of application state
const selectSearchState = state => state.imageSearch

// a selector for returning the entire results array or `null`
// if it should be nothing or if there were no results
export const selectAllResults = createSelector(
  selectSearchState,
  searchState => {
    // we can contain this check to see if we're loading
    // here instead of everywhere
    if (searchState.loading || !searchState.results) {
      return null
    }
    return searchState.results
  }
)

// Now our selector to show current results gets a bit simpler
export const selectCurrentResults = createSelector(
  selectSearchState,
  selectAllResults,
  (searchState, results) => {
    if (!results) {
      return null
    }

    const { currentPage, resultsPerPage } = searchState

    // now our `searchState` here is
    // *just* the relevant portion and
    // we don't have to think about whether it's loading
    // or if it has any results at all. That's already
    // handled.

    // now we can figure out where we
    // are at and return the right subset of results.
    // Array.prototype.slice works well for this, we just
    // need our start index and end index.
    const startIndex = resultsPerPage * currentPage
    const endIndex = startIndex + resultsPerPage

    return results.slice(startIndex, endIndex)
  }
)

// we can also add a selector for whether or
// not to show "next page" and "previous page" controls:
export const selectPaginationLinks = createSelector(
  selectSearchState,
  selectAllResults,
  (searchState, results) => {
    // you'll see this pattern a lot where we
    // returning `null` early on if some
    // basic condition isn't met.
    if (!results) {
      return null
    }

    // again, we only get here if we're not currently
    // fetching and we have current results.
    // So we don't have to handle that condition here
    // at all.
    const { currentPage, resultsPerPage } = searchState

    // if we have more results than the last item on our current page
    // (assuming current page is zero-indexed)
    const hasMore = results.length > (currentPage + 1) * resultsPerPage
    const hasPrevious = currentPage > 0

    return {
      showNext: hasMore,
      showPrevious: hasPrevious
    }
  }
)

It may be helpful to think about what happens in this scenario when there is a change in the application state.

Let's first clarify a few things. Let's assume we're using the last two selectors:

  1. selectPaginationLinks
  2. selectCurrentResults

We'll apply them to a connect()'ed component as follows:

// first we import them
import { selectPaginationLinks, selectCurrentResults } from './image-search/selectors'

// The select function that will use our selectors
// to extract properties to be passed to the component
const select = state => ({
  paginationLinks: selectPaginationLinks(state),
  currentResults: selectCurrentResults(state)
})

// Our actual component
const SearchResultsPage = ({ paginationLinks, currentResults }) => (
  ...
)

// exporting our connected result
export default connect(select)(SearchResultsPage)

Let's think through a few scenarios

First, let's describe each step in the process. Every component that you connect() that's in our component tree will get store from the context and register a callback function by calling subscribe() on the store. Then when an action is dispatched, the following steps will occur:

  1. Every connected component's subscribe callback gets called.
  2. The callback with call store.getState() to get the current application state.
  3. The entire application state will be passed to the select function that we wrote for that component.
  4. The result of our select function is an object. Each key from this object will be passed to the component as "props."
  5. The component's shouldComponentUpdate lifecycle method will compare each prop and return false if they're all the same.
  6. Since the component was told not to update by shouldComponentUpdate, it will not even call the render method, or in the case of a functional component, it won't call the function.

So, this means that any time any action is dispatched the above steps will occur even if nothing that matters to this component has changed.

Now you can start to see why the select() function that we use to connect components can have a significant performance impact. We only want it to select what we need so we don't render when we don't need to.

Scenario #1: an overly generic select function

If our select function looked like this, what would happen?

const select = state => state

Well, anytime we update state in Redux, it gets replaced. So what would happen in this scenario is:

  1. The connected component would call the select function after each action, as before.
  2. It would compare each "key" that we returned. Since our select function here returns the entire application state it would compare state stored in every top-level reducer.
  3. If anything in the entire application state changed the component would re-render, which is unnecessary and inefficient.

Scenario #2: A well-written, specific select function created with reselect

To clarify, this is the scenario I'm describing:

const select = state => ({
  paginationLinks: selectPaginationLinks(state),
  currentResults: selectCurrentResults(state)
})
  1. The connected component would call the select function after each action, as before.
  2. It would compare each value in the object returned by our select function.
  3. If any of these very specific values are different, the component will re-render, but not in any other case!

As it turns out, this is precisely what we want it to do. We only want the connected component to do the work of re-rendering if state that it uses has changed.

Let's dig even deeper to follow the execution path when using selectors created with reselect.

  1. selectPaginationLinks will get called and will be passed the entire application state.
  2. It has two input selectors: selectSearchState and selectAllResults.
  3. Remember selectSearchState is a super simple function that just plucks the right reducer's value from the application state object: state => state.imageSearch. That gets called and returns the relevant "slice" of our state.
  4. The other input selectAllResults will get called, and again, it too gets passed the entire application state.
  5. selectAllResults also uses the simple input function that selects the relevant "slice" of state.

So what does this all mean?

If state.imageSearch has changed at all, the result function will be re-evaluated. Otherwise, the functions created by reselect will return their cached result. In this way, even if you build a crazy dependency tree of nested selectors 50 layers deep, and even if lots of different selectors get re-used as input functions for other selectors, the result is that none of the selectors created by reselect will run more than once per state change. This is what makes reselect so powerful.

It lets you inexpensively isolate pieces of logic into small, simple functions with precise inputs and outputs and then piece them together to derive complex answers from your state.

The one exception here is that at the root of all this you'll need a function that simply grabs the relevant portion of the state from the application state. This is akin to a spreadsheet cell that contains a raw value, instead of a formula. This function should be as simple as state => state.someValue. Ideally, you shouldn't do any more "work" than that in the top-level function because that part will be re-evaluated for every run of a selector that depends on it.

Thinking through these steps also helps us see why we should ensure that actions are only dispatched for worthwhile things. Remember, we only want to dispatch things that are actually "newsworthy," because there's potentially a lot of code that will be re-evaluated with each dispatch. Similarly, if you have some state that only one little section of app cares about, perhaps just keep it as local component state? For example, things like in-progress form state, what inputs have what value, in my opinion, should not live in Redux. Sure, when the user submits a form and state gets submitted to a server or something, that's probably an action dispatch. But dispatching actions for when a user changes a form value likely isn't "newsworthy" to the rest of your app and should just be kept as local state.

I've built a simple, runnable example of the pagination selectors above that writes out to the console when each result function is executed. This way you can see how reselect limits the amount of work done. The example is located at: https://reduxbook.com/selectors

Real world example of building complex behavior using selectors

I was contracted to join an effort to help Starbucks modernize their stack. One of the first things they were tackling was rebuilding the "store locator" functionality on Starbucks.com to use React and Redux instead of the mostly server-side driven .net platform they were using previously.

The designs called for a large map as the primary interface and a search box much like what you'd see on Google Maps. But of course, the only results would be Starbucks store locations.

I joined when a good chunk of this store locator app had already been built. It had a fairly traditional approach where the root component triggered data fetches, etc. But as we tried to add more "smarts" we'd often have trouble keeping things running smoothly; we'd kept inadvertently introducing what I like to refer to as "state bugs" when we made changes. These are bugs where some action would cause the wrong thing to happen, or put the app into an incorrect state. For example, the results getting cleared inadvertently due to some other action, or re-fetching results unnecessarily when none of the search parameters had changed to the point where it should have re-fetched. As we continued to implement additional filtering and more complex behavior, it was too easy to break other features in subtle ways.

As it turned out, despite its apparent simplicity, this app had a surprising amount of logic and various factors to account for. Here's a rough sampling:

  1. Do we have permission to request a user's geolocation?
  2. If a user was on the "bare" URL (no existing search params) and we had geolocation we should search around their geolocation, and then zoom and center the map as needed to include the relevant, nearby results.
  3. If we didn't have geolocation, we should focus/zoom on the right country based on IP address.
  4. If we zoomed too far out, we shouldn't show any result.
  5. We should fetch new results if the user manually moved the map past a certain threshold—but the threshold varied based on current zoom level and density of current results being displayed, if any.
  6. The URL should always be shareable. So, as users move the map, we wanted to maintain an up-to-date URL query string that would produce the same results if shared by someone. This was done by constantly modifying the URL with history.replaceState()
  7. If the page was loaded with search parameters in the URL, we also had to go the other way and apply the relevant search criteria to reproduce the same results as expected based on reading the URL.
  8. If we had search parameters that were too restrictive for the given results, but we had other results available we needed to indicate that in a message.
  9. If a single, particular Starbucks location was selected it should be highlighted visually on the map, and we should keep things somewhat "locked," in that moving the map a bit shouldn't trigger fetches.
  10. We had to keep a carousel of current results in list form overlaid on the map, in addition to the "pins" displayed on the map, and highlight the right location as you hovered the carousel.

You get the point. There's a fair amount of factors in play. Not only did we have to manage all that state, but many of the "rules" that governed how the app should behave changed somewhat based on the current state.

As you can imagine, it was all the edge cases that made things challenging. Inevitably we'd change some aspect of it but end up subtly breaking another piece of functionality. We had a lot of conditional logic throughout the components that would determine what could and could not be done. I strongly felt that we should attempt to centralize and isolate that logic into discrete chunks outside of our components, so I began working on refactoring it into a consolidated tree of selectors.

Ultimately, we ended up with a few high-level selectors like:

  1. currentUrlSelector which would produce the URL the user should be seeing based on the current set of parameters and map location. We could take the result of this, and update the browser's URL to match it.
  2. apiUrlSelector This would build up the API URL with the proper query parameters that we should use to fetch data based on the current state of the app.
  3. shouldFetchResults selector that took into account all the various things that affected this, such as whether the user had moved the map past the threshold given the current set of parameters, whether we had a user's geolocation, etc.

These were built on a myriad of smaller selectors that would encapsulate little portions of the logic. One, for example, just returned whether or not the map had been moved more than the current movement threshold for the current zoom level and result density. That selector could then be used as an input for another selector, and all that logic could be simplified to a hasUserMovedMap argument.

By isolating all these pieces of logic into small, simple, pure functions that just returned an answer to a specific question we were able to reduce the complexity of the components in the app dramatically. Rather than having logic sprinkled throughout, we had a set of selector functions that we could easily write comprehensive tests for.

This dramatically improved our confidence in what we were shipping. It also enabled the team to make significant changes to the UI design, as they later did, without having to re-visit all this logic. Components were no longer responsible for encapsulating application logic; they merely had to be connected to the right selectors.

Neat, but does this have real business value?

I sometimes hear developers say things like "real apps are messy, deal with it" or "you're being paid to produce functioning apps, not pretty code." While I can appreciate where they're coming from, I think that sentiment misses a key point:

Well-structured, encapsulated, "pretty code," allows you to add features that would otherwise be too difficult to build! It lets you add a level of "smarts" and dependability to your app that can put the experience far ahead of the competition.

If you've ever felt a sense of dread when asked to add a new feature to an app, you know what I'm getting at. I know I've been there many times before. It's unfortunate when you're pushing back on useful features just because you know how much technical complexity was involved. But, often this happens when you feel like what you've built is a leaning tower of code. You're worried that adding anything else to the top of it will make it all come crashing down. We often need a better way of managing that complexity. For me, "spreadsheet-style programming" using nested selectors has completely changed how I code.

My point is that these are not merely philosophical musings of a developer in some ivory tower:

  • The ability to adapt to changing requirements without causing regressions has real business value.
  • The ability to add increasingly complex features without increasing "technical debt" has real business value.
  • The ability to ask complex questions of your data has real business value.

I alluded to this a bit in the Starbucks example, but we can extend this power of selectors and use them to trigger other actions. This is easily one of the most powerful patterns I've stumbled across in the last few years, which is why I've dedicated an entire chapter to this concept later on in the book.

Chapter recap

  1. Selectors are an official, unofficial part of Redux. They are how we read state from our app.
  2. Anytime you find yourself updating a "calculated" or "derived" value as part of handling a particular action type in a reducer, you should probably be deriving that answer using a selector instead.
  3. A demo of deriving complex data using selectors can be found in the mortgage calculator example here: https://reduxbook.com/mortgage
  4. Selectors can potentially be run a lot so they should be as efficient as possible to avoid unnecessary work.
  5. Reselect lets us derive increasingly complex answers while containing complexity. These patterns allow us to efficiently answer incredibly complex questions of our state by isolating chunks of logic into their separate selectors.
  6. Selector composition is much like a spreadsheet, where we simply reference other cells instead of having to copy and paste their formula everywhere we want to use it.
  7. A runnable example of nested selectors is located at: https://reduxbook.com/selectors

Part 1 recap

This concludes the "Redux basics" part of the book. As a recap, we've covered:

  • What is application state and what is Redux?
  • How we store and update state in reducers
  • Triggering actions with action creators
  • Understanding middleware and enhancers
  • Binding state to views
  • Selecting relevant state for our views and asking questions of our state with selectors

In Part 2 we're going to go deeper into patterns and abstractions.

In my opinion, this is where things start to get really exciting. Let's dig in.

results matching ""

    No results matching ""