Better State Management with Redux Selectors

Better State Management with Redux Selectors

Introduction to selectors

State is the core of any React application, it is where all of your data is stored and manipulated. However, sometimes we may receive chunky data that may need to be trimmed down or raw data that may need some manipulation before it can be used in your React application. GraphQL APIs may help with chunky data as they allow the client to specify which pieces of data they would like returned from the server. However, not all APIs make use of GraphQL and some that do may still require further manipulation on the data before it can be consumed by the client. This is where selectors come in.

But what are selectors? A selector is simply a function that takes in a state value and returns a derived state.

The derived state can either be a trimmed down copy of state containing only the required value(s) or a piece of state that has been manipulated in some way such as a calculation or formatting.

Why use selectors?

Selectors are great for a number of reasons, all of which come together to make state as efficient as possible. According the reselect docs, here are some reasons you may want to make use of selectors in your code:

  • Selectors can compute derived data, allowing Redux to store the minimal possible state. This allows us to leave out information we may not require from our client application.
  • Selectors are efficient. A selector is not recomputed unless one of its arguments changes.
  • Selectors are composable. They can be used as input to other selectors. This is useful in cases where one piece of information is used as a baseline for several calculations for data to be displayed.

Using Selectors with React and Redux

To show how selectors can help with state management, we’ll build a simple React app with some dummy state data that we will be manipulating.

Application setup

First let’s bootsrap our application with create-react-app, if you do not have it installed already you can do so by running:

bash
npm install -g create-react-app $$

Now you can create your application by running:

bash
create-react-app selector-demo $$

We have a few dependencies that we will need for our application, to install them, run this command:

bash
npm install redux react-redux redux-devtools-extension reselect $$

These packages each carry out the following functions.

  • redux
    • state management
  • react-redux - acts as a link between React and Redux
  • redux-devtoools-extension - allows us to debug redux from the browser
  • reselect
    • allows us to use selectors with redux

The next step is to create our folder structure. In the src directory, create two more directories, components and redux which will hold our React components and Redux logic respectively, In the redux directory, create another two directories, reducers and selectors which just as the names suggest, will hold our reducer and selector logic respectively.

We can now add our mock data to a new file in the redux directory, create a file called initialState.js and add the following code.

js
const state = { db: { info: { name: { first: "Anakin", last: "Skywalker" }, physical: { height: 75, weight: "Classified :)", medical: { vacinnated: true, terminalConditions: ["Asthma", "Chronic Burns"] } } } }, random: { fact: { nature: "The sky is blue", dev: "Undefined is not a function" }, fiction: { nature: "Sharks can drown", dev: "Javascript is Java for scripts" } } }; export default state;

For our Redux setup, we’ll pass on adding actions logic to speed up our development, we will instead initialize our state directly from our reducers rather than dispatching actions to do so. For this reason our reducers will be rather different. Keep in mind this is not recommendable for an actual app but for our case we want to focus on selectors.

Create a file called personReducer.js inside the reducers folder and place this code in it,

js
import state from "../initialState"; const reducer = () => { return state.db.info; }; export default reducer;

This returns the info object from our mocked initial state which holds information about a person. To do the same for the random object which holds random information, create randomReducer.js and add this code.

js
import state from "../initialState"; const reducer = () => { return state.random; }; export default reducer;

Combine the reducers using the combineReducers function from Redux, this allows them to be used as a single root reducer will be passed to our redux store for use. To do this, create an index.js file in the reducers directory, import the two reducers and pass them to combineReducers and default export the result as follows.

js
import { combineReducers } from "redux"; import personal from "./personalReducer"; import random from "./randomreducer"; export default combineReducers({ personal, random })

We can now set up our redux store configurations.

js
import { createStore, applyMiddleware } from "redux"; import { composeWithDevTools } from "redux-devtools-extension"; import reducer from "./reducers/index"; const enhancer = composeWithDevTools(applyMiddleware()); export default function configureStore(initialState) { const store = createStore(reducer, initialState, enhancer); return store; }

The applyMiddleware function takes a list of middlewares to be added to the store as an argument. This is where we would add thunks or sagas in our react application when making use of actions. The result is then passed to composeWithDevTools to create an enhancer that will be applied to our store.

The createStore function takes 3 arguments and uses them to generate a redux store to hold our data:

  1. reducer (Function): A reducing function that returns the next state tree, given the current state tree and an action to handle.
  2. [preloadedState] (any): The initial state. If you produced reducerwith combineReducers, this must be a plain object with the same shape as the keys passed to it. Otherwise, you are free to pass anything that your reducer can understand.
  3. [enhancer] (Function): The store enhancer. You may optionally specify it to enhance the store with third-party capabilities.

We will then pass the configure and pass the store to our app through a by editing the index.js file in the src directory.

jsx
import React from "react"; import ReactDOM from "react-dom"; import { Provider } from "react-redux"; import configureStore from "./redux/store"; import "./styles.css"; import Display from "./components/Display"; const store = configureStore({}); function App() { return ( <Provider store={store}> <div className="App"> <h1>Selectors Demo</h1> <Display /> </div> </Provider> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);

We call configureStore and pass an initial state object to it, which in our case is an empty object. The Provider is a higher order component that passes the store to the App similar to how the Context API in React works. You will also notice a Display component has been imported above, we are yet to develop it but it will be connected to our redux store in order to display our data. Since our store is all set up we can get into this.

Create a Display.jsx file in your components and create your component.

jsx
import React from "react"; const Display = ({ nature, dev, fullName, height }) => { console.log({ dev }); return ( <div> <div> <strong>Fact or Fiction?</strong> //Place Random data here </div> <br /> <br /> <br /> <div> <strong>My Info</strong> // Place Person data here </div> </div> ); }; export Display;

We are now all set to pass the data with our selectors.

Working with selectors

Before we get to use our selectors we must first compose them. We will look at both memoized and non-memoized selectors.

A non-memoized selector simply takes the state object and returns a derived value from it. To illustrate this we will create two simple selectors that pick pieces of our random data. Create a randomSelector.js file inside the selectors direcory and create and export two functions in it, selectRandomFactNature and selectRandomFictionDev to pick values from state as shown:

js
export const selectRandomFactNature = state => state.random.fact.nature; //returns "The sky is blue" export const selectRandomFictionDev = state => state.random.fiction.dev; // returns "Javascript is Java for scripts"

A memoized selector takes the pieces of the state object and only recalculates its values if these pieces change rather than doing so whenever a piece of state changes making the application more efficient. To show this we’ll make use of parts of the info object in state. Create a personalSelector.js file and have the following logic in it:

js
import { createSelector } from "reselect"; const selectFirstName = state => state.personal.name.first; //Anakin const selectLastName= state => state.personal.name.last; //Skywalker export const selectFullName = createSelector( selectFirstName, selectLastName, (firsName, lastName) => `${firsName} ${lastName}` // Anakin Skywalker ); const selectHeight = state => state.personal.physical.height; //75 export const selectHeightInFeet = createSelector( selectHeight, height => height / 12 //6.25 );

In the code above we have three non-memoized selectors, selectFirstName, selectLastName selectHeight which pick pieces of state. These selectors are then used as inputs to other memoized selectors created using reselect.

Reselect provides a function createSelector for creating memoized selectors. createSelector takes an array of input-selectors and a transform function as its arguments. If the Redux state tree is mutated in a way that causes the value of an input-selector to change, the selector will call its transform function with the values of the input-selectors as arguments and return the result. If the values of the input-selectors are the same as the previous call to the selector, it will return the previously computed value instead of calling the transform function.

We have two transform functions, in selectFullName we have one that takes the results of selectFirstName and selectLastName and concatenates them into a single string which it then returns. Meanwhile selectHeightInFeet takes the result of selectHeight which is simply the heigh in inches, divides it by 12 to get the height in feet and returns that value.

That’s it, our selectors are ready, all we have to do now is link them to our Display component. To do this we’ll make use of the connect function from react-redux as well as the createStructuredSelector from reselect.

createStructuredSelector is a convenience function for a common pattern that arises when using Reselect. The selector passed to a connect decorator often just takes the values of its input-selectors and maps them to keys in an object

Import it along with connect and all our selectors into the Display.jsx file. Pass an object with the selectors mapped onto names you wish to use for the data returned by them in props to the createStructuredSelector function to get your mapStateToProps object which you will then pass to the connect function as shown below.

jsx
import React from "react"; import { connect } from "react-redux"; import { selectRandomFactNature, selectRandomFictionDev } from "../redux/selectors/randomSelectors"; import { selectFullName, selectHeightInFeet } from "../redux/selectors/personalSelectors"; import { createStructuredSelector } from "reselect"; const Display = ({ nature, dev, fullName, height }) => { console.log({ dev }); return ( <div> <div> <strong>Fact or Fiction?</strong> <br /> <br /> Nature: {nature} <br /> <br /> Dev: {dev} </div> <br /> <br /> <br /> <div> <strong>My Info</strong> <br /> <br /> Full Name: {fullName} <br /> Height: {height} Ft </div> </div> ); }; const mapStateToProps = createStructuredSelector({ nature: selectRandomFactNature, dev: selectRandomFictionDev, fullName: selectFullName, height: selectHeightInFeet }); export default connect(mapStateToProps)(Display);

We then have an object of our props passed as an argument to the functional component and that’s it, the data is now usable within the component. We managed to trim down a massive state object and keep only the data we need as well as carrying out some calculations and store it in the format we need. All thanks to the magic of selectors. When you run the application, it should look something like this:

https://codesandbox.io/embed/selectors-demo-swf9d?fontsize=14

Conclusion

Selectors are great, they not only simplify state management but they also allow us to manipulate data and pass it to our components in the required format. This makes them a great option for use in projects where there are pre-built components that require data to be passed to them in a specific format. Selectors also make applications more efficient through memoization.

Read similar articles