DEV Community

Dylan Grandmont
Dylan Grandmont

Posted on

It's OK to useStore with React-Redux

I recently had two members of my team independently confuse themselves with a subtle point in the react-redux bindings. We have a few instances where a component includes callbacks and inside of those callbacks we need to compute some values from our redux state. We take those values and dispatch an action:

import { useDispatch } from 'react-redux';

function Component() {
  const dispatch = useDispatch();

  function callback() {
    const value = ... // requires us to compute a value from the state
    dispatch(someAction(value))    
  }

  return <div onClick={callback} ... ></div>
}
Enter fullscreen mode Exit fullscreen mode

There are two relevant APIs in react-redux here: useSelector and useStore. useSelector accepts a selector, a function which computes a value from state; when that value changes, the component will re-render.

useStore on the other-hand, provides access to the redux store within component, but it will not re-render the component on any state changes.

The confusion I've seen comes from a small comment within the react-redux docs:

[useStore] should probably not be used frequently. Prefer useSelector() as your primary choice. However, this may be useful for less common scenarios that do require access to the store, such as replacing reducers.

I think this statement makes sense. The primary use-case is to connect your component to a store so that when a particular part of the store changes, the component re-renders. useSelector achieves this and that is the intent of the statement. However, it's easy to misunderstand this as useStore is "discouraged" and this causes trouble.

Suppose you interpret the statement, as my team members did, as "useSelector should always be preferred". In the above example, this will produce a solution like:

import { useDispatch, useSelector } from 'react-redux';

function Component() {
  const dispatch = useDispatch();
  const value = useSelector(someSelector) // compute value from state

  function callback() {
    dispatch(someAction(value))    
  }

  return <div onClick={callback} ... ></div>
}
Enter fullscreen mode Exit fullscreen mode

But this doesn't make sense. We don't need to re-render the component when value changes! Nothing in the rendered output displays the result of value. We only need to evaluate value when the callback is executed. And if value changes frequently in the state, then we are doing a lot of re-renders that we don't need to.

This is one of those "less common scenarios" where we do want access to the store directly:

import { useDispatch, useStore } from 'react-redux';

function Component() {
  const dispatch = useDispatch();
  const store = useStore();

  function callback() {
    const value = someSelector(store.getState())
    dispatch(someAction(value))    
  }

  return <div onClick={callback} ... ></div>
}
Enter fullscreen mode Exit fullscreen mode

This allows the UI to update only when needed and for the correct value to be computed just-in-time, when the callback is executed.

Top comments (6)

Collapse
 
markerikson profile image
Mark Erikson • Edited

Yeah, "use a value that isn't going to change" is a relatively rare use case. In most cases people do want to get updates, and part of the point of that warning I wrote is that some folks have tried to call store.getState() without realizing that it will not re-render the component when there's a change.

Note that in cases where you need to access Redux state in a callback, that may be a good use case for just moving all that logic into a thunk, since thunks have access to getState.

FWIW, you can actually (ab)use dispatch to implement the "read but not subscribe" use case, by having passing a selector to a thunk, then having the thunk immediately return the selector result :)

const selectTodos = state => state.todos;

const readSomeState = (selector) => {
  return (dispatch, getState) => {
    return selector(getState());
  }
}

// later
function MyComponent() {
  const dispatch = useDispatch();

  // THIS IS HACKY, and yet it works, legally!
  const todos = dispatch(readSomeState(selectTodos));

  // render here
}
Enter fullscreen mode Exit fullscreen mode

I'm not sure I necessarily recommend this approach, but it's syntactically and semantically valid.

Collapse
 
dominikdosoudil profile image
Dominik Dosoudil

I think that thunk (or saga) won't cover all cases. Sometimes you don't need global state and you are just fine with useState. In that case you can't use thunk to access the store and useStore is the best option IMO.

Collapse
 
markerikson profile image
Mark Erikson

I.... have no idea what you're trying to say there.

The article is discussing the React-Redux useStore hook. useState has nothing to do with this.

Sagas aren't even part of this discussion.

Thread Thread
 
dominikdosoudil profile image
Dominik Dosoudil

I am talking about following scenario. In case you want get data from store, do something with them on demand (user click) and save them into local store, you cannot use thunk. I know that I might store it into redux instead of local state, but sometimes it's just better option.


const Foo = () => {
  const [computed, setComputed] = useState(0);
  const store = useStore();
  const recompute = useCallback(() => {
    const data = store.getState();
    setComputed(doSomething(data));
  });
  return <>...</>
};
Enter fullscreen mode Exit fullscreen mode

Did I make my point clear?

Collapse
 
dylangrandmont profile image
Dylan Grandmont

Thanks for taking a look at the post @markerikson ! I was considering making a PR to the react-redux docs to update the wording but it sounds like in general this is not a common use case for users.

Collapse
 
dominikdosoudil profile image
Dominik Dosoudil

I got the same idea but I couldn't find any other article about it except confusing docs as you mentioned and confused devs on SO.
So thank you for writing it down.