7

I'm using the ultimate combination of React + Redux + Reselect + Immutable.js in my application. I like the idea of reselect because it lets me keep my state (maintained by the reducers) as simple as possible. I use a selector to calculate the actual state I need which is then fed to the React components.

The problem here is that a small change in once of the reducers causes the selectors to recalculate the whole derived output and as the result also the whole React UI is updated. My pure components don't work. It's slow.

Typical example: The first part of my data comes from server and is basically immutable. The second part is maintained by the client and is mutated using the redux actions. They are maintained by separate reducers.

I use a selector to merge both parts into a single list of Records which is then passed to the React components. But obviously, when I change a single thing in one of the objects, the whole list is regenerated and new instances of Records is created. And the UI is completely re-rendered.

Obviously running the selector every time is not exactly efficient but is still reasonably fast and I'd be willing to make that trade off (because it does make the code way simpler and cleaner). The problem is the actual rendering which is slow.

What I'd need to do would be to deep merge the new selector output with the old one because Immutable.js library is smart enough not to create new instances when nothing was changed. But as selectors are simple functions that do not have access to previous outputs, I guess it's not possible.

I assume that my current approach is wrong and I'd like to hear other ideas.

Probably the way to go would be to get rid of reselect in this case and move the logic into a hierarchy of reducers that would use incremental updates to maintain the desired state.

tobik
  • 7,098
  • 7
  • 41
  • 53
  • Are you sure you are properly assigning unique key props to each item in the list? React should optimize the rendering (see https://facebook.github.io/react/docs/multiple-components.html). – bryanph Mar 20 '16 at 13:38
  • 1
    No, the problem here is that the selector generates a completely new set of Records (that means new instances) regardless of whether they changed or not. That's why it gets always re-rendered. – tobik Mar 20 '16 at 21:24
  • Even so, you say the performance hog does not happen when in the selector but while rendering. I think the react diffing algorithm should avoid unnescessary re-renders when providing a unique key (see https://facebook.github.io/react/docs/reconciliation.html). Not sure whether the diffing algorithm works on reference changes though. – bryanph Mar 20 '16 at 22:04
  • I think we're talking about different problems. You're linking the diff algorithm handling the reconciliation between the generated React tree and the actual DOM tree. What I'm dealing with is the reconciliation between the current state (let's say props passed to components) and the React tree. I'm trying to achieve that the React tree won't change in the first place unless there is an actual change in the state. The React's diff algorithm is fast but not fast enough and this is sometimes necessary to get usable performance. See https://facebook.github.io/react/docs/pure-render-mixin.html – tobik Mar 20 '16 at 22:44

3 Answers3

6

I solved my problem but I guess there is no right answer as it really depends on a specific situation. In my case, I decided to go with this approach:


One of the challenges that the original selector handled nicely was that the final information was compiled from many pieces that were delivered in an arbitrary order. If I decided to build up the final information in my reducers incrementally, I'd have to make sure to count with all possible scenarios (all possible orders in which the information pieces could arrive) and define transformations between all possible states. Whereas with reselect, I can simply take what I currently have and make something out of it.

To keep this functionality, I decided to move the selector logic into a wrapping parent reducer.

Okay, let's say that I have three reducers, A, B and C, and corresponding selectors. Each handles one piece of information. The piece could be loaded from server or it could originate from the user on the client side. This would be my original selector:

const makeFinalState(a, b, c) => (new List(a)).map(item => 
  new MyRecord({ ...item, ...(b[item.id] || {}), ...(c[item.id] || {}) });

export const finalSelector = createSelector(
  [selectorA, selectorB, selectorC],
  (a, b, c) => makeFinalState(a, b, c,));

(This is not the actual code but I hope it makes sense. Note that regardless of the order in which the contents of individual reducers become available, the selector will eventually generate the correct output.)

I hope my problem is clear now. In case the content of any of those reducers changes, the selector is recalculated from scratch, generating completely new instances of all records which eventually results in complete re-renders of React components.

My current solution looks lite this:

export default function finalReducer(state = new Map(), action) {
  state = state
    .update('a', a => aReducer(a, action))
    .update('b', b => bReducer(b, action))
    .update('c', c => cReducer(c, action));

  switch (action.type) {
    case HEAVY_ACTION_AFFECTING_A:
    case HEAVY_ACTION_AFFECTING_B:
    case HEAVY_ACTION_AFFECTING_C:
      return state.update('final', final => (final || new List()).mergeDeep(
        makeFinalState(state.get('a'), state.get('b'), state.get('c')));

    case LIGHT_ACTION_AFFECTING_C:
      const update = makeSmallIncrementalUpdate(state, action.payload);
      return state.update('final', final => (final || new List()).mergeDeep(update))
  }
}

export const finalSelector = state => state.final;

The core idea is this:

  • If something big happens (i.e. I get a huge chunk of data from the server), I rebuild the whole derived state.
  • If something small happens (i.e. users selects an item), I just make a quick incremental change, both in the original reducer and in the wrapping parent reducer (there is a certain duplicity, but it's necessary to achieve both consistency and good performance).

The main difference from the selector version is that I always merge the new state with the old one. The Immutable.js library is smart enough not to replace the old Record instances with the new Record instances if their content is completely the same. Therefore the original instances are kept and as a result corresponding pure components are not re-rendered.

Obviously, the deep merge is a costly operation so this won't work for really large data sets. But the truth is that this kind of operations is still fast compared to React re-renders and DOM operations. So this approach can be a nice compromise between performance and code readability/conciseness.

Final note: If it wasn't for those light actions handled separately, this approach would be essentially equivalent to replacing shallowEqual with deepEqual inside shouldComponentUpdate method of pure components.

tobik
  • 7,098
  • 7
  • 41
  • 53
1

This kind of scenario can often be solved by refactoring how the UI is connected to the state. Let's say you have a component displaying a list of items: instead of connecting it to the already built list of items, you could connect it to a simple list of ids, and connect each individual item to its record by id. This way, when a record changes, the list of ids itself doesn't change and only the corresponding connected component is re-rendered.

If in your case, if the record is assembled from different parts of the state, the selector yielding individual records could itself be connected to the relevant parts of the state for this particular record.

Now, about the use of immutable.js with reselect: this combination works best if the raw parts of your state are already immutable.js objects. This way you can take advantage of the fact that they use persistent data structures, and the default memoization function from reselect works best. You can always override this memoization function, but feeling that a selector should access its previous return value if often a sign that it is in charge of data that should be hold in the state / or that it is gathering too much data at once, and that maybe more granular selectors could help.

VonD
  • 5,075
  • 2
  • 20
  • 30
0

It looks like you are describing a scenario very close to the one why I wrote re-reselect.

re-reselect is a small reselect wrapper, which initializes selectors on the fly using a memoized factory.

(Disclaimer: I'm the author of re-reselect).

Andrea Carraro
  • 9,731
  • 5
  • 33
  • 57