DEV Community

Anya Brun
Anya Brun

Posted on

My misadventures with the useSelector hook

One of the more interesting aspects of completing my React/Redux project was learning about the mechanics of state.

What is state and why did I need it?

React state is a JavaScript object that contains information that is private and fully controlled by the component. The Redux library allows for a global application state. Instead of (or, sometimes, in conjunction with) each component's local state, there is one object that contains all the state for the application. This object is located in the Redux store.

I decided to use Redux Toolkit to help set up the global state of my application. While all the information contained in the state is located in the store, an individual component may not need the entire state object, but only a piece of the information it contains.

Redux Toolkit conceptualizes these "pieces of state" as slices and provides the createSlice function as a way to assemble the application state one slice at a time. Slices are organized by the different features of the application, so that each slice contains only the initial state that is relevant to a specific feature.

Physically organizing the slices that make up state into features helped me understand what information was better stored in local vs. global state. This separation also made it easier to visualize the discrete steps of each feature and the corresponding actions + reducers I needed to write.

When it comes down to using data from the global state, it is best practice to write selector functions. The React-Redux library provides the useSelector hook, allowing you to use the selector function within a component. The selector takes the entire Redux store state as its argument, reads (and/or derives) some value from the state, and returns the result. According to the Redux docs, using selector functions keeps state minimal and easy to read, while making sure that original state is not being replaced.

How did I implement state?

In my React/Redux app, the two major features, for which I need access to the global store, are loading the levels on the Home page and letting users submit their own levels.

To implement these features, I created two slices: levelsSlice.js and formSlice.js. The former GETs game levels from the API and stores them in the state object, while the latter handles POSTing user-submitted data. State is structured thus:

{
    levels: 
        {loading: false, hasErrors: false, levels: []},
    form: 
        {loading: false, hasErrors: false, message: ""}
}
Enter fullscreen mode Exit fullscreen mode

In addition to generating and exporting actions, the thunk action, and the reducer itself--I decided to create the selector functions in these files, as well. Here is the code for levelsSlice.js:

export const levelsSelector = state => state.levels 
Enter fullscreen mode Exit fullscreen mode

Frankly, the levelsSlice was the first slice I created, so I relied heavily on the Redux Toolkit "Getting Started" guide. In other words, I knew what this code was supposed to do, but I didn't know how it worked.

Well, this initial setup got my app to work perfectly insofar as it fetched the correct data and saved it to the store, so users could choose and play levels.

My next task was to make it possible for users to submit their own levels. On to formSlice.js. I more or less followed the same process for writing the thunk, the reducers, etc as in levelsSlice--with one small change:

export const formSelector = state => state.message
Enter fullscreen mode Exit fullscreen mode

My reasoning: I would need the message to display as a validation to the user, so I should read it from the state with a selector.

Again, everything worked perfectly. A user could submit a hint and a word, this data would be POSTed to the API, and--upon returning to the Home page, where all the levels are displayed--the user-submitted level would appear, ready to play.

However, in the console I was getting an error to the effect of unable to deconstruct property 'message' (something, something) undefined. The error pointed to the following line:

const { message, loading, hasErrors } = useSelector(formSelector) 
Enter fullscreen mode Exit fullscreen mode

What went wrong?

First, I compared formSlice and levelsSlice, looking for any syntactical errors I may have made that would cause the code to break. Finding none, I tried to assign each property to a constant individually and see if it was just one not being assigned properly (meaning the problem could be in one of the reducers). All three lines gave the same error. After a couple more fruitless attempts at debugging, I googled the error and looked through some Stack Overflow posts.

One such post in particular (titled UseSelector State is Undefined)--while not directly answering my question--gave me an idea that sparked my understanding of the problem (and, subsequently, its solution). A reply suggested that OP should "[a]dd a console for debugging and check the state object." I did so with the following line of code:

useSelector((state) => console.log(state))
Enter fullscreen mode Exit fullscreen mode

Thereafter, I (as expected) saw logged to the console the state object, whereupon I realized my folly.

I hadn't realized that the selector function accepts the entire state. In levelsSlice.js, I mistakenly assumed that the state object I used in the selector was the specific slice of state represented by the initial state I had defined in levelsSlice.js. I must have gotten confused by the fact that the name of the slice (and, hence, the key in the global state that points to the slice's properties) was the same as the key for the levels array.

The line in which I deconstruct the properties message, loading, and hasErrors returned undefined because state.message does not exist. What I really meant was state.form.message.

How did I fix it?

At this point, I understood that the actual point of the selector in formSlice.js was to select the slice of state that stores the properties relevant to the form feature (i.e., state.form). Using the object destructuring syntax, I can then assign the value of each of those properties to a constant in my form component. With that newfound knowledge, I refactored formSlice like so:

export const formSelector = state => state.form
Enter fullscreen mode Exit fullscreen mode

Using this selector function in the useSelector hook would return the following object:

{ message: "", loading: false, hasErrors: false } 
Enter fullscreen mode Exit fullscreen mode

And logging message, loading, and hasErrors to console would allow me to see those values and how they change before, during, and after a user submits the form.

Debugging this error was one of the more fun parts of building my project. Having recently watched the film "Murder on the Nile", I felt like Hercule Poirot, following the clues until I eventually caught the culprit and solved the mystery. Thankfully, the only thing killed in this case was time.

Top comments (0)