Selectors with React + Redux-ORM. Passing data to subcomponents

1.8k Views Asked by At

I've got a React application and I'm trying to use Reux-ORM and I'm struggling with selectors. I've got a simple app with normalized data

// /models/team.js
import { Model, many, attr } from 'redux-orm';
import Player from './player';

class Team extends Model {
  static reducer(action, Team, session) {
    [...]
  }
}
Team.modelName = "Team";
Team.fields = {
  id: attr(),
  players: many('Player;),
}

and

// /models/player.js
import { Model, attr } from 'redux-orm';

class Player extends Model {
  static reducer(action, Player, session) {
    [...]
  }
}
Player.modelName = "Player";
Player.fields = {
  id: attr(),
}

I register them into the ORM as follows:

// /selectors/selector.js
import { ORM } from 'redux-orm';

import Team from '../models/team';
import Player from '../models/player';

const orm = new ORM();
orm.register(Team, Player);

export default orm;

So far so good. Now I would like to have a React Component that renders a list of all teams with all players. So the first thing I did was to write a selector:

// /selectors/selector.js
import { createSelector } from "redux-orm";
import orm from "../models/index";

export const teams = createSelector(
  orm,
  state => state.orm,
  session => {
    return session.Team.all().toRefArray();
  }
);

This gives a list of teams, but not yet its associated players. So in my component, I can now use teams from my props with if I add a function const mapStateToProps = state => ({teams: teams(state)});

But now my question is, what's the best way to get the team-specific players? Do I add them to the returned teams in the selector? Or do I write a separate selector? And would I pass on the players to the the component directly, or rather make a separate component in the Team component?

class Teams extends React.Components {
   render() {
    return this.props.teams.map(team => {
      /* Option 1: */
      const teamPlayer = team.players // returned by initial selector
      /* Option 2: */
      const teamPlayers = [some call to selector based on 'team'];
      /* Option 3: */
      // Don't pass on the players but the id instead
      return (
        <Team
          /* Option 1 & 2: */
          players= {teamPlayers}
          /* Option 3: */
          teamId = {team.id} // and within the Team component make a selector call
        />
   }
}

Implementationally, I got stuck at making the selectors and couldn't find examples for this case for redux-orm v0.19, and conceptually I'm still not sure which way is the best to go. All help is much appreciated!

1

There are 1 best solutions below

2
On

Update (2019-09-30):

Since v0.14 there is a much simpler way to create common selectors. First of all, the teams selector can be written like this:

const teams = createSelector(orm.Team);

Similar to what you would do with a QuerySet you can create a selector to retrieve a team's players as an array of references:

const teamPlayers = createSelector(orm.Team.players);

Using react-redux you can simply pass a single team to a React component which then fetches all related data upon store updates.

function TeamList(props) {
  const teams = createSelector(state => teams(state));
  return (
    <ul>
      {teams.map((team, i) => (
        <Team key={team.id} team={team} />)
      )}
    </ul>
  );
}

function Team({ team }) {
  const players = useSelector(state => teamPlayers(state, team.id));
  return (
    <div>
      {team.name}: {players.map(player => player.name).join(', ')}
    </div>
  );
}

You could also use the teamPlayers selector in the parent component but in my experience it's usually better for reusability if a component retrieves its dependencies by itself. So sometimes it may even be better to pass team.id to Team instead of the entire team ref. Didn't do it here because it would seem useless.

Be aware that teams(state) returns all teams in the order of insertion. Do not try to index into it like teams(state)[team.id] as the team.id does not typically match the team's index in the array when iterating. To fetch a single team pass its primary key like teams(state, team.id). For more details please take a look at our new docs.


The short answer is: there is no right way. It has been asked here before.

However, the easiest way to do this is to append the player references directly like so:

export const teams = createSelector(
  orm,
  state => state.orm,
  ({ Team }) => {
    return Team.all().toModelArray().map(team => ({
      ...team.ref,
      players: team.players.toRefArray(),
    }));
  }
);

This would allow you to access your players directly as an array property of your team objects, and you wouldn't need to add anything else. It has the downside of possible overhead in case you want to reuse the selector without accessing the players array, though. In your view you could either pass the whole team reference or parts of it as a prop:

<Team
  // either entire team object
  team={team}

  // or each field individually
  teamId={team.id}
  players={team.players}
/>

Alternatively you could create a separate selector for each relation you need to query:

export const teamPlayers = createSelector(
  orm,
  state => state.orm,
  ({ Team }) => {
    return Team.all().toModelArray().reduce(map, team => ({
      ...map,
      [team.id]: team.players.toRefArray(),
    }));
  }
);

<Team
  // either entire team object
  team={team}
  teamPlayers={teamPlayers}

  // or each field individually
  teamId={team.id}
  players={teamPlayers[team.id]}
/>

This has the fairly obvious problem of needing to add a selector or at least the minimum boilerplate each time you want to use a relationship accessor.

Calling selectors within components themselves is not the way to do it as only mapStateToProps will be called when your store updates. I would strongly advise against the third option unless you are willing and have a good reason to connect again in the child component.

If you feel that all of this is kind of messy, you are definitely not alone -- I have been thinking about how to improve this for a while now. We may be able to have Redux-ORM provide an easier way to create or access these kinds of selectors that everyone needs. Right now it is very flexible but not exactly convenient, yet.