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:
bashnpm install -g create-react-app
$
Now you can create your application by running:
bashcreate-react-app selector-demo
$
We have a few dependencies that we will need for our application, to install them, run this command:
bashnpm 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.
jsconst 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,
jsimport 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.
jsimport 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.
jsimport { 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.
jsimport { 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:
reducer
(Function): A reducing function that returns the next state tree, given the current state tree and an action to handle.- [
preloadedState
] (any): The initial state. If you producedreducer
withcombineReducers
, this must be a plain object with the same shape as the keys passed to it. Otherwise, you are free to pass anything that yourreducer
can understand. - [
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.
jsximport 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.
jsximport 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:
jsexport 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:
jsimport { 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 aconnect
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.
jsximport 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.