Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Redux: More containers v.s. less containers

As I get further into implementing redux + react into a fairly complex app which depends on many API requests to load a single page, I'm having trouble deciding whether it's better to have a single container component at the root of the page which handles all async stuff and passes props down to dumb components, v.s. having multiple container components which concern themselves only with the data they need, as well as fetching the data they need. I've gone back and forth between these two patterns and found that they each have pros/cons:

If I put a single container component at the top:

  • pro: All isFetching props and fetchSomeDependency() actions can be handled in one place.
  • con: the downside which is really annoying is that I find myself having to forward props and callbacks through multiple components, and certain components in the middle of the tree end up being tied up to this.

Here's a visual example of the issue that shows the relationships required props-wise:

<MyContainer
  data={this.props.data}
  isFetchingData={this.props.isFetchingData}
  fetchData={this.props.fetchData}
 >
   {!isFetchingData && 
     <MyModal
        someData={props.data}
        fetchData={props.fetchData}
      >
       <MyModalContent
         someData={props.data}
         fetchData={props.fetchData}
       >
          <SomethingThatDependsOnData someData={props.someData} />
          <SomeButtonThatFetchesData  onClick={props.fetchData} />
        </MyModalContent>
      </MyModal>
    }
 </MyContainer>

As you can see, <MyModal /> and <MyModalContent /> now need to be concerned with props that have nothing to do with it, seeing as a modal should be able to be re-used and only be concerned with stylistic qualities of a modal.

At first the above seemed great but once I got to 100+ components it all felt very tangled, and I found the complexity of these top-level container components to be too high for my liking, seeing as most of them (in the app I'm working on) depend on responses from 3+ API requests.

Then I decided to try multiple containers:

  • pro: Completely removes the need to forward props. It still makes sense to do it in some cases, but it's a lot more flexible.
  • pro: Way easier to refactor. I'm surprised at how I can significantly move around and refactor components without anything breaking, whereas in the other pattern things broke a lot.
  • pro: The complexity of each container component is much less. My mapStateToProps and mapDispatchToProps is more specific to the purpose of the component it's in.
  • con: Any component that depends on async stuff will always need to handle isFetching state in itself. This adds complexity that is not necessary in the pattern where its handled in a single container component.

So the main dilemma is that if I use one container, I get this un-necessary complexity in components between the root container and the leaf components. If I use multiple containers, I get more complexity in the leaf components, and end up with buttons that need to worry about isFetching even though a button should not be concerned about that.

I'd like to know if anyone has found a way to avoid both cons, and if so, what is the "rule of thumb" you follow to avoid this?

Thanks!

like image 635
Cooper Maruyama Avatar asked Sep 07 '16 19:09

Cooper Maruyama


5 Answers

The way I have always seen it is to have your containers at the top most component of a logical components group other than your root/app component.

So if we have a simple search app that display results and lets assume the component heiarchy is such

<Root> <- setup the app
  <App>
    <Search/> <- search input
    <Results/> <- results table
  </App>
</Root>

I would make Search and Results redux aware containers. Because react component are suppose to be composable. You might have other components or pages that need display Results or Search. If you delegate the data fetch and store awareness to the root or app component, it make the components become dependent on each other/app. This make it harder down the line when you have to implement changes, now you have to change all the places that use them.

The exception to this is probably if you do have really tightly coupled logic between components. Even then, I would say then you should create a container that wraps your tightly coupled components since they won't be abled to be used realistically without each other.

like image 57
Dan Avatar answered Oct 26 '22 23:10

Dan


Redux author Dan Abramov suggests that you use container components when you need them. That is, once you get to have too many props wiring up and down between components it's time to use containers.

He calls it an "ongoing process of refactoring".

See this article: https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

like image 27
michaelpoltorak Avatar answered Oct 26 '22 22:10

michaelpoltorak


I wouldn't even consider using a single container approach. It pretty much entirely negates all advantages of redux. There is no need whatsoever to have a state management system if all your state is in one place and all your callbacks are in one place (root component).

I think there's a thin line to walk, though. I'm making an app where I've been at it for about 5 weeks (part time) and it's up to 3000 lines right now. It has 3 levels of view nesting, a routing mechanism i implemented myself, and components that are 10+ levels of nesting deep that need to modify state. I basically have one redux container for each big screen and it works marvelously.

However, if I click on my "clients" view, I get a clients listing which is fine, since my clients view is inside a redux container and gets the list of clients passed as props. However, when I click on one client, I'm really hesitant to do another redux container for the individual client's profile since it's only one additional level of passing props. It seems that depending on the scope of the app, you might want to pass props up to 1-2 levels past the redux container and if it's any more than that, then just create another redux container. Then again, if it's an even more complex app, then the mixing of sometimes using redux containers and some other times not using them could be even worse for maintainability. In short, my opinion is trying to minimize redux containers wherever possible but definitely not at the expense of complex prop chains, since that's the main point of using redux to begin with.

like image 32
Victor Moreno Avatar answered Oct 27 '22 00:10

Victor Moreno


So it's been over 2 years since I've posted this question, and this whole time I have been consistently working with React/Redux. My general rule of thumb now is the following: Use more containers, but try to write components in such a way where they don't need to know about isFetching.

For example, here is a typical example of how I would have built a to-do list before:

  function Todos({ isFetching, items }) {
    if (isFetching) return <div>Loading...</div>

    return (
      <ul>
        {items.map(item => 
          <li key={item.id}>...</li>
        )}
      </ul>
    )
  }

Now I would do something more like:

  function Todos({ items }) {
    if (!items.length) return <div>No items!</div>

    return (
      <ul>
        {items.map(item => 
          <li key={item.id}>...</li>
        )}
      </ul>
    )
  }

This way, you only have to connect the data, and the component has no concerns about states of asynchronous API calls.

Most things can be written this way. I rarely need isFetching, but when I do it is typically because:

  1. I need to prevent, for example, a submit button from being clicked a second time, which makes an API call, in which case the prop should probably be called disableSubmit rather than isFetching, or

  2. I want to explicitly show a loader when something is waiting for an asynchronous response.

Now, you might think, "wouldn't you want to show a loader when items are being fetched in the above todos example?" but in practice, actually I wouldn't.

The reason for this is that in the above example, let's say you were polling for new todos, or when you add a todo, you "refetch" the todos. What would happen in the first example is that every time this happened, the todos would disappear and get replaced with "Loading..." frequently.

However, in the second example that is not concerned with isFetching, the new items are simply appended/removed. This is much better UX in my opinion.

In fact, before posting this, I went through all the UI code for an exchange interface I wrote which is quite complex and did not find a single instance of having to connect isFetching to a container component that I wrote.

like image 20
Cooper Maruyama Avatar answered Oct 26 '22 22:10

Cooper Maruyama


You don't have to dispatch AND load your state in the same place.

In other words, your button can dispatch the async request, while another component can check if you're loading.

So for example:

// < SomeButtonThatFetchesData.js>

const mapDispatchToProps = (dispatch) => ({
  onLoad: (payload) => 
    dispatch({ type: DATA_LOADED, payload })
});

You'll need to have some middleware to handle a loading state. It needs to update isFetching when you're passing an async payload.

For example:

const promiseMiddleware = store => next => action => {
  if (isPromise(action.payload)) {
    store.dispatch({ type: ASYNC_START, subtype: action.type });

Then you can use it wherever you want:

// <MyContainer.js>

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

And load the data in your inner nested component:

// <SomethingThatDependsOnData.js>

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

like image 42
realraif Avatar answered Oct 27 '22 00:10

realraif