Skip to content

Instantly share code, notes, and snippets.

@itsjoekent
Last active March 9, 2018 15:00
Show Gist options
  • Save itsjoekent/4c04a5b5e468cff39c180975376e444f to your computer and use it in GitHub Desktop.
Save itsjoekent/4c04a5b5e468cff39c180975376e444f to your computer and use it in GitHub Desktop.
Set of guidelines for instrumenting your React apps

This is intended only as a proposal and not a set of final guidelines. It assumes you have working knowledge of React & Redux. Last updated March 8th, 2018.

Theory

This document outlines a system for developing React applications that should be able to provide a clean and natural way of thinking about data within your UI and how it binds to components. The system should try to get out of the way as much as possible, and make it simple to refactor your application when the time arises. Finally it should keep things separate from eachother, and make it clear how each piece can be independently tested.

Specific Problems-to-solve

These are problems specific to React/Redux applications and is not meant to be a critique of any specific project.

1. Keeping data consistent across components.

Imagine you have a data model called User which looks like the following,

{
  "id": 1,
  "email": "[email protected]",
  "firstName": "Joe",
  "lastName": "Kent",
  "photo": "http://78.media.tumblr.com/tumblr_m3i2kerw6o1qejbiro1_1280.png",
}

You then create a component to display the user in a module,

const User = (props) => {
  const { firstName, lastName, photo } = props;
  
  return (
    <div>
      <h1>{firstName}, {lastName}</h1>
      <img src={photo} />
    </div>
  );
};

User.propTypes = {
  firstName: PropTypes.string.isRequired,
  lastName: PropTypes.string.isRequired,
  photo: PropTypes.string.isRequired,
};

User.mapStateToProps = (state) => ({
  firstName: state.user.firstName,
  lastName: state.user.lastName,
  photo: state.user.photo,
});

This works fine until the API team decides to make some changes to the user model. For example, let's say that firsrtName and lastName get moved under a new object with the key name.

{
  "name": {
    "first": PropTypes.string.isRequired,
     "last": PropTypes.string.isRequired,
  },
  "photo": PropTypes.string,
}

This change would require us to update our User component, because firstName is no longer a valid prop. When this happens to one component, it's not a show-stopper. But imagine if we also had a Comment component which had a user prop as well.

const Comment = (props) => {
  const { user, message } = props;
  const { firstName, lastName } = user;
  
  return (
    <div>
      <p>{ message }</p>
      <h3>{ firstName }, { lastName }</h3>
    </div>
  );
};

Now this component also has to be refactored in how it references the name data, along with it's asscociated prop types. The first step to solving this should be creating selectors, which are nothing more than functions that accept an entire state and select a specific value from it. In the case of our user, it would look something like this,

// before
function selectUserFirstName(state) {
  return state.user.firstName;
}

// after
function selectUserFirstName(state) {
  return state.user.name.first;
}

// ...

User.mapStateToProps = (state) => ({
  firstName: selectUserFirstName(state),
});

In turn, this means if the original component recieved its props from the selector function it would have never broken. In the case of the Comment component, however, it's still going to be borked. This is because the Comment component is recieving the entire user as a prop, and is directly accessing the name values based on the outdated key name.

There are two solutions to this problem, depending on what you're looking to solve. In the case of the comment, where the amount of properties is sparse and well defined, you can simply bind the users first and last name directly to the component rather than the user object.

In many cases however this isn't feasible. For example, imagine if there was a component that stored all of the comments on a blog or social post of some kind.

const CommentFeed = (props) => {
  const { comments } = props;
  
  return comments.map(comment => <Comment key={comment.id} {...comment} />);
}

You could bind every instance of firstName as a new prop by prepending the comment id to the key name, but I think we all agree that would look awful. This is where chain_ selectors come into play (I'm not sure if that's a real term or I just made it up). Chain selectors simply means that the selector functions can be composed together easily depending on your situation. The defining principles should be that,

  1. A selector should always use another selector to reference it's parent.
  2. There should be a version of the selector that retrieves the value from an object, not the Redux state.

In the case of the user, it would look something like this,

// Before
function selectUserFromState(state) {
  return state.user;
}

function selectFirstNameFromUser(user) {
  return user.name.first;
}

function selectUserFirstNameFromState(state) {
  return selectFirstNameFromUser(selectUserFromState(state));  
}

// After
function selectUserFromState(state) {
  return state.user;
}

function selectFirstNameFromUser(user) {
  return user.name.first;
}

function selectUserFirstNameFromState(state) {
  return selectFirstNameFromUser(selectUserFromState(state));  
}

Now your components can reliably access any property from any data source. However, we still haven't addressed a few sub-problems, and we also invented a new one, extensive boiler-plate.

The good news is we can likely automate much of the common selectors one would need.

2. File layout and reducing friction to find what you're looking for.

3. Seperating "functional" components from "presentational" components. Along with how & when to re-use components.

4. Where do I put the logic for < X >?

5. Frontend build config (K.I.S.S.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment