Can I write a selector without knowing the sliced reducer path?

Viewed 411

I already have some prior experience using React and Redux with JavaScript, but I just started a project using TypeScript. While reading the documentation for redux-toolkit, I found out about the idea of creating selectors to facilitate getting the data you need from the state tree.

Now, from the official documentation, the guidelines to write a selector in a slice require you to already know how the store will name this slice. For example, from the Create React App redux-typescript template:

export const selectCount = (state: RootState) => state.counter.value;

Notice that the counter from state.counter is only named in the root reducer file:

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

This seems like a really bad idea to me, since if I change the name of the slice in the root reducer, I'll need to go back to the slice itself and update this new name there too. Also, it seems like an overkill to get all the RootState when I don't need anything from another part of the state.

So, can I write a selector that only sees the state of the current slice, and let the combineReducers function to deal with its address? Example:

export const selectCount = (state: CounterState) => state.value;

If not, is there a better way to read a specific value from the state without needing to know the exact structure of the store?

2 Answers

Update: I have published an npm package for this.

selector function is passed to useSelector / mapStateToProps, which calls your selector function with complete(global) state data. This means, your selector function needs to handle the actual store object and not just your slice object.

If you are changing the store structure in combine reducer, obviously your store data signature will also change.

But if you still want to solve this issue, you can create selector functions for each of your slice using some helper function like below:

// helper method for our helper createGlobalSelector
const getNestedObject = (obj, ...keys) => keys.reduce((acc, cur) => acc[cur], obj);

// example of getNestedObject
// getNestedObject({a: {b: 32}}, 'a', 'b')
console.log(getNestedObject({ a: { b: 32 } }, 'a', 'b')); // 32

// Util function to create selector functions for a slice
// accepts an object of selector functions, and key structure for that store slice
// returns modified selector functions, which will work with useSelector / mapStateWithprops
const createGlobalSelector = (selectorFuncsObj, ...sliceStructure) =>
  Object.fromEntries(
    Object.entries(selectorFuncsObj).map(([key, selectorFunc]) => [
      key,
      (state) => selectorFunc(getNestedObject(state, ...sliceStructure))
    ])
  );

// example of createGlobalSelector
// store structure: { a: { b: { x: 55, y: 65, z: 75 } } }
// selectX: (state) => state.x
// selectY: (state) => state.y
// selectZ: (state) => state.z

const { selectX, selectY, selectZ } = createGlobalSelector(
  { selectX: (state) => state.x, selectY: (state) => state.y, selectZ: (state) => state.z },
  'a', 'b'
);

const store = { a: { b: { x: 55, y: 65, z: 75 } } };

console.log(selectX(store)); // 55

console.log(selectY(store)); // 65

console.log(selectZ(store)); // 75

In the above example, createGlobalSelector creates selector functions, which will handle the actual store structure and you don't need to worry about it.

Now, implementing your selector with the createGlobalSelector helper

const selectorFuncObj = { selectCount : (state) => state.value }

const sliceStructure = 'counter'


// selector function which can be exported and used inside `useSelector` / `mapStateToProps`
export const { selectCount } = createGlobalSelector(selectorFuncObj, sliceStructure)

// store: {counter: {value: 5}}
const count = useSelector(selectCount) // 5

Update: After searching on Internet, how people are solving this issue, I found this article. The two approaches mentioned in the article are good for one-level deep store structure, but it is cumbersome for more than one level nesting. The util function above handles deeply nested store structure well, and In my opinion, solves the issue. Also, check this SO answer.

You can Create a const with your slice name which will be used in your selectors, so if you want to change your slice name you can do that easily.

const sliceName = "counter"
export const selectCount = (state: RootState) => state[sliceName].value;

Related