7

Let's say we have the old traditional way of React / Redux: (you don't need to expand the code if you are familiar with it:)

import React from 'react';
import { connect } from 'react-redux';

function Count(props) {
  return (
    <div>
      <button onClick={props.increment}> + </button>
      {props.count}
      <button onClick={props.decrement}> - </button>
    </div>
  );
}

const mapStateToProps = state => ({
  count: state.count
});

const mapDispatchToProps = dispatch => ({
  increment: () => dispatch({ type: 'INCREMENT' }),
  decrement: () => dispatch({ type: 'DECREMENT' })
});

export default connect(mapStateToProps, mapDispatchToProps)(Count);

Now, using React Hooks useSelector() and useDispatch(), the above code could be written as this way:

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

function Count() {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();

  const increment = () => dispatch({ type: 'INCREMENT' });
  const decrement = () => dispatch({ type: 'DECREMENT' });

  return (
    <div>
      <button onClick={increment}> + </button>
      {count}
      <button onClick={decrement}> - </button>
    </div>
  );
}

export default Count;

Both versions work exactly the same, by themselves, except, isn't version 1 highly reusable for Count? That's because using a different mapStateToProps() and mapDispatchToProps(), we can use connect() again to create another CountNoodle() and now we have reused Count().

For version 2, Count() is hard-wired with what state and dispatch it uses, so the whole Count() is entirely not-reusable. That is, it has to be used with that particular state and particular dispatch, but nothing else. Isn't it true? So is version 2 above not recommended and actually you would have a version 3, which is not to call it Count() but called it CountNoodle() and "wire up" the state and dispatch, and re-use Count(), which would be simply "presentational"?

So it may look something like this:

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

// Count() actually would be in a different file and CountNoodle.js
//   would import that file
function Count({count, increment, decrement}) {
  return (
    <div>
      <button onClick={increment}> + </button>
      {count}
      <button onClick={decrement}> - </button>
    </div>
  );
}

function CountNoodle() {
  const count = useSelector(state => state.countNoodle);
  const dispatch = useDispatch();

  const increment = () => dispatch({ type: 'INCREMENT_NOODLE' });
  const decrement = () => dispatch({ type: 'DECREMENT_NOODLE' });

  return <Count ...{count, increment, decrement} />;
  // or   return Count({count, increment, decrement});
}

export default CountNoodle;
nonopolarity
  • 146,324
  • 131
  • 460
  • 740
  • Maybe, depends on needs. But if you think about it, isn't that *really* all the `connect` HOC is? A container component to map redux state and actions to props a presentational component consumes? – Drew Reese Apr 21 '20 at 07:44
  • I think if depends on needs, then the usual need is that we should be able to reuse code... because if our need is 2 observers all the time, we don't even need to use the observer pattern -- we can just hard wire everything, and when data change, always call the 2 functions. So if we say, there is only one `Count` in the whole app, we can do whatever we want, hard wire everything, but that usually is not a software "best practice" – nonopolarity Apr 21 '20 at 08:25

2 Answers2

1

I addressed this question in my post Thoughts on React Hooks, Redux, and Separation of Concerns and my ReactBoston 2019 talk on "Hooks, HOCs, and Tradeoffs".

I'd encourage you to read/watch through both of them, but as a summary:

  • React hooks in general lead you to a different set of organizational patterns than HOCs do, where components are more responsible for handling their own data fetching requirements internally
  • This does result in a different set of tradeoffs than HOCs offer, in terms of reusability and testability
markerikson
  • 63,178
  • 10
  • 141
  • 157
1

Hoc version (version 1) of your Count component is also "hard-wired" with particular state and dispatch. You are doing it when export default connect(mapStateToProps, mapDispatchToProps)(Count);

To compare both hoc and hook approaches to achieve Count reuse lets take a look at your original Count:

function Count(props) {
  return (
    <div>
      <button onClick={props.increment}> + </button>
      {props.count}
      <button onClick={props.decrement}> - </button>
    </div>
  );
}

And to reuse it with hocs you'll go like:

const withCount = (field, Component) => connect(
  state => ({ count: state[field] }), 
  dispatch => ({
    increment: () => dispatch({ type: 'INCREMENT', payload: field }),
    decrement: () => dispatch({ type: 'DECREMENT', payload: field })
  })
)(Component)

const NoodleCount = withCount("noodle", Count)
const DrinksCount = withCount("drinks", Count)

And for the hooks you could:

function useCount(field) {
  const count = useSelector(state => state[field]);
  const dispatch = useDispatch();
  const increment = () => dispatch({ type: 'INCREMENT', payload: field });
  const decrement = () => dispatch({ type: 'DECREMENT', payload: field });

  return {count, increment, decrement}
}

const NoodleCount = () => <Count {...useCount("noodle")} />;
const DrinksCount = () => <Count {...useCount("drinks")} />;

Obviously, there are pros and cons for both of approaches but:

Does using React Hooks drastically reduce how code can be reused in React / Redux?

No, hooks can not be the source of troubles in component reusability.

Hope it makes sense

n1stre
  • 5,856
  • 4
  • 20
  • 41
  • I am trying to understand... how come you a `useCart()` hook and it is hard wire to `NOODLE_COUNT` but is some kind of "cart" but not a "count"... is it a cart as in shopping cart? `CountNoodle()` uses `` with a `value` but `Count` doesn't take a `value`... – nonopolarity Apr 21 '20 at 18:06
  • yeah, I thought of "noodle" as some item in shopping cart, but the names here may vary for each domain, it doesn't matter. I just forgot to add the `value` prop, sorry. – n1stre Apr 21 '20 at 18:11
  • the `count` actually can be a component on the page, just keeping track of how many noodles the customer want to order... and the count of drinks, and count of snacks... so 3 counts in an app, for example – nonopolarity Apr 21 '20 at 18:17
  • then it doesn't make any sense to me.. what is `increment` and `decrement` for in this case? – n1stre Apr 21 '20 at 18:19
  • I'm pretty sure you don't store components in redux state – n1stre Apr 21 '20 at 18:20
  • `increment` is, for example, the waitress touch a button to increment the number of noodles the customer wants, and it'd show on the screen... `Count` is actually a pretty simple counter component... – nonopolarity Apr 21 '20 at 18:23
  • I think you re-factored it so that creation of new components is even easier. Is the `with` in `withCount()` syntax actually common in React/Redux usage? I think my version 3 also addressed the reusability issue... I haven't tested the actual code but basically it is to have HOC: specify what the state and dispatch is, and then pass to a pure, innocent, presentational component – nonopolarity Apr 21 '20 at 20:48
  • `with` is common for [HOC](https://reactjs.org/docs/higher-order-components.html) naming – n1stre Apr 21 '20 at 20:54