Is it possible to create a React Hook factory function?

Viewed 4491

I have a React app that relies heavily on redux, react-redux, and redux-saga. I'm starting to experiment with React Hooks, and the useSelector and useDispatch hooks in react-redux, and I've run into an issue with how to essentially write a factory function to generate hooks for each of my redux nodes.

In general, I have a redux node for each API endpoint my app consumes. There are about 100 unique endpoints in this app, and so there are about 100 redux nodes. Each of those nodes then corresponds to one state-[node].js file, like state-users.js, etc. Those state files each encapsulate the endpoint they should call, trigger sagas to handle the HTTP lifecycle (start, fail, success, etc), and so on.

Over time, I've written code that abstracts much of the boilerplate away into utility functions, including functions that auto generate action creators, selectors, reducers, and the connect function. It's a bunch of code, and somewhat convoluted, but the gist looks something like this. First, I set up an array of objects describing actions that are possible for this redux node. A simplified version looks like this:

const messages = [
  { action: 'GET', creator: 'get', connect: true },
  { action: 'POST', creator: 'post', connect: true },
  { action: 'CLEAR', creator: 'clear', connect: true },
];

This describes that there will be three actions, get , post, and clear, and that they should be exposed in the connector. I have a set of common reducers (e.g. most get reducers are identical across nodes), so those are assumed here based on name.

Then I set up a list of selectors, like this: const selectorKeys = ['data','pending','errors'];

...and then I have a factory function that I feed these arrays into, which looks something like this:

const { connector } = stateGenerators({
   keyword: 'users', //redux node keyword
   messages: messages,
   selectorKeys: selectorKeys

}) 

This is a simplified version of how everything really works, but it's the crux of it. Again, all of the above code is abstracted into a state file, like state-users.js.

Then, in my class component, I just need to import the connector from state-users.js, like this:

import { connector } from 'state-users';

class Users extends Component {
    componentDidMount() {
       this.props.get();
    }
    componentWillUnmount() {
      this.props.clear();
    }
    render () {
      const { data } = this.props;
      return (
        <div>
        {data.map()}
        </div>

      )
    }
}

export connector()(Users)

This model does get clunky at times, but the nice part is that nearly all of the redux boilerplate is abstracted into common files, so my individual state files are, for the most part, really simple.

Now, to the question: is it possible to do something like this "factory function" approach with Hooks? So far in my experimenting I have not figured out how to do this. There are two fundamental issues:

  • First, you can't put hooks in loops, so I can't do this:
const selectors = {}
const reduxNodeKeyword = 'users';
['data','pending','errors'].map((selectorKey) => {
   selectors[selectorKey] = useSelector((state) => state[keyword].selectorKey);
})

That code results in this error: React hook "useSelector" cannot be called inside of a callback.

In practice, that means I can't just pass in an array of selector keys I'd like and then have it spit back my selectors.

  • Second, you can't put hooks inside conditionals. So since the first idea failed, I tried a different approach in my factory function, which looks something like this:
 if (_.includes(stateSelectors, 'data')) {
    result['data'] = useSelector((state) => state[keyword].data);
 }

That results in this error: React hook "useSelector" is called conditionally. React Hooks must be called in the exact same order in every component render

So that's also a bummer. But I think what I'm left with is that for each of my 100 Redux nodes, I would have to write a custom, and verbose, hook to more or less replicate connect.

I know, I know, hooks should encourage me to think differently, but I still need to fetch data from the server and provide it to components, and I need to follow that same basic pattern 100 times. Even if hooks makes my components more elegant (as I expect it would), the thought of writing 100 or so hooks out by hand, each with a fair amount of repeated data, rather than somehow auto-creating them using some sort of factory approach, gives me hives.

Help?

Thanks!

1 Answers

Not sure if this will be useful for others, but I found an approach that works for me. I can't put hooks in iterators, nor in if statements, but I do have common patterns all over the place. So the answer was to abstract those common patterns into generic hooks. For example, I have a hook called useCommonReduxListSelectors, which looks like this:

export const useReduxListCommonSelectors = (keyword: string) => {
  return {
    data: useSelector((state: any) => state[keyword].data),
    errorMessage: useSelector((state: any) => state[keyword].errorMessage),
    filters: useSelector((state: any) => state[keyword].filters),
    pending: useSelector((state: any) => state[keyword].pending),
    totalCount: useSelector((state: any) => state[keyword].totalCount)
  };
};

I have lots of Redux state nodes that are responsible for handling lists returned from an API, and the data shape of most of those list endpoints is what you see above. So then the hook that a component would invoke uses useReduxListCommonSelectors, like this:

export const useReduxState = ({ id, defaultValues }) => {
  const selectors = useReduxListCommonSelectors({
    keyword:'users'
  });

  return {
    ...selectors,

  };
};

And then obviously useReduxState can have any other data in there (actionCreators, custom selectors, etc) that is required by that node. So it allows me to abstract the most common patterns into reusable hooks, and also have the flexibility to add custom code to each useReduxState instance as needed.

Related