LoginSignup
18
6

More than 5 years have passed since last update.

React is not a new kid on the block anymore. This year version 16 was released to much fanfare, but if you read the change log, you'll find nothing revolutionary. There are some great new additions like React portals, error boundaries and (get this) the ability to return an array in the render method, but these are incremental improvements. We're not talking Angular vs jQuery. At this point, you might think the React core team have been resting on their laurels, but you'd be wrong. Version 16 includes a ground-up rewrite of the core architecture, with no breaking API changes. Take that, Angular 2.

While switching to new framework is nice every few years, React is somewhere mid-lifecycle which makes it a good time to be an established React developer. Many have gotten to grips with the fundamentals and are taking the opportunity to refine their skills. I'm one of them, and in this post I'll share with you the 3 tips that have improved my apps the most in 2017.

1: Make use of React Children

As your React applications grow, you may find that your component tree becomes very tall. If you have ever added a new method to a stateful container, and had to pass that method through several components so that you can use it in a nested component, you'll feel my pain here. Using React children is a great way to keep the tree shallow.

In this case, we'll create a super convenient component for grid layouts.

class Grid extends Component {
  render = () => (
    <div style={{ display: "grid" }}>
      {React.Children.map(this.props.children, (child, index) => (
        <div
          style={{
            gridColumnStart: index % this.props.columns,
            gridColumnEnd: index % this.props.columns + 1
          }}
        >
          {child}
        </div>
      ))}
    </div>
  );
}

By wrapping each child in a styled div, we can invisibly place our list items into a grid. So what does it look like when we use this in practice?

const ProductList = ({ products }) => (
  <Grid columns={2}>
    {products.map(product =>
      <ProductTile product={product} />
    )}
  </Grid>
);

Easy, right? Just render your items into the grid and the layout is handled for you. React children is a powerful feature of the framework which will change the way you think about writing components. Try overlaying tooltips, passing attributes read from the DOM as props, or keeping your use of styling classes DRY by creating simple styled wrappers. The possibilities are endless!

2. Flatten your state

When you start out building highly interactive apps with React, it's very tempting to take JSON from an API and start manipulating it in the shape that you receive it. When making changes to that state immutably, this can become very long-winded and error-prone. Your setState might look a little like this:

class App extends Component {
  togglePublicEmail = () => this.setState(prevState => ({
    userProfile: {
      ...prevState.userProfile,
      flags: {
        ...prevState.userProfile.flags,
        publicEmail: !prevState.userProfile.flags,
      },
    },
  }))
}

While this works fine, it will make your life much easier if you create utility functions to flatten your state, then re-create its original structure when you POST it to your API. Because these functions are easy to test, you're much less likely to run into problems. Here's what that might look like:

const flattenState = state => ({
  userName: state.userProfile.userName,
  userEmailIsPublic: (
    state.userProfile.flags
    &&  typeof state.userProfile.flags === 'boolean'
  ) ? state.userProfile.flags : null,
});

const reconstructState = flatState => ({
  userProfile: {
    userName: flatState.userName,
    flags: {
      flatState.userEmailIsPublic,
    },
  },
});


class App extends Component {
  togglePublicEmail = () => this.setState(prevState => ({
    userEmailIsPublic: !prevState.userEmailIsPublic,
  }))
}

This is an 'obvious when you know' sort of technique which I like a lot. There are situations where you spend much more time changing your state than you do importing/exporting it, so it can definitely be worth taking the time to simplify your data structure. It's also more performant since you aren't creating multiple new objects for every state change.

3. Use Reselect with Redux to keep state minimal

Statefulness is a messy business – avoiding it wherever possible is usually your best option. It follows that we should keep our state as simple as possible, and derive any values we need as we need them. That's where Reselect comes in.

Reselect gives you a composable, memoized way of calculating values from your reducer state. Let's try it out with an unread message counter component. When we receive our list of messages, we could calculate our unread count and store it in our state, but storing derived values is messy, remember? We'll start with a simple React app which calculates its unread count on the fly to avoid storing it in state:

import React, { Component } from 'react';

const UnreadCountBubble = ({ unreadCount }) => <p>{unreadCount}</p>;

export default class App extends Component {
  state = [
    { hasBeenRead: true, messageText: 'Foo' },
    { hasBeenRead: false, messageText: 'Bar' },
    { hasBeenRead: false, messageText: 'Baz' },
  ]

  render() {
    const unreadCount = this.state.messages.reduce(
      (count, message) => count + message.hasBeenRead,
      0
    );

    return <UnreadCountBubble unreadCount={unreadCount} />;
  }
}

Here, we're using the component's internal state to store the messages and calculating the unreadCount on every render. This looks okay, so what's the problem? Optimizing the performance of React applications almost always involves the render method. We want to avoid rendering when we can, and we want to minimize the amount of work done in the render method. In this example, we are iterating over an array and declaring a variable for every render. This could be triggered by a prop or state change totally unrelated to the unreadCount, so why are we re-calculating the value?

If we move the messages into a Redux reducer, the structure changes a little but the same problems exist. For readers who haven't used Redux, I'll cover the basics.

import React, { Component } from 'react';
import { connect } from 'redux';

const UnreadCountBubble = ({ unreadCount }) => <p>{unreadCount}</p>;

class AppComponent extends Component {
  render() {
    const unreadCount = this.props.messages.reduce(
      (count, message) => count + message.hasBeenRead,
      0
    );

    return <UnreadCountBubble unreadCount={unreadCount} />;
  }
}

const mapStateToProps = (state, ownProps) => ({ messages: state.messages });

const App = connect(mapStateToProps, AppComponent);

export default App;

Our messages array is now stored outside of the component in a reducer (not shown). To access this state, we use a mapStateToProps function passed to Redux's connect method. Every time the reducer state changes mapStateToProps is passed a new copy of the state and the component's props. Any properties on the object returned by the function are added to the component's props (note that we're now mapping over this.props.message instead of this.state.messages).

Now we can start start using Reselect. It's a simple library so the code changes are minimal, which is great for people already using Redux.

import React, { Component } from 'react';
import { connect } from 'redux';
import { createSelector } from 'reselect';

const UnreadCountBubble = ({ unreadCount }) => <p>{unreadCount}</p>;

class AppComponent extends Component {
  render() {
    return <UnreadCountBubble unreadCount={this.props.unreadCount} />;
  }
}

const getMessages = state => state.messages;
const unreadMessageCount = createSelector(
  getMessages,
  messages => messages.reduce(
    (count, message) => count + message.hasBeenRead,
    0
  ),
);

const mapStateToProps = (state, ownProps) => ({ unreadCount: unreadMessageCount(state) });

const App = connect(mapStateToProps, AppComponent);

export default App;

Now, instead of calculating the unreadCount inside of our component, that's handled by the reselector before the data even reaches our component. This gives us a neat separation of concerns: reducers store our raw state, our reselectors calculate any derived values and our components render them. Plus, because we're using pure functions, Reselect will only recalculate unreadCount when its arguments change. No performance cost!

In truth, using the reselector pattern is more important than the library itself but using the two together is a no-brainer for Redux apps. Try it out!

18
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
6