Skip to content

Instantly share code, notes, and snippets.

@joeytwiddle
Last active May 20, 2020 05:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save joeytwiddle/8634b729a50b816762f78aa545fbd72f to your computer and use it in GitHub Desktop.
Save joeytwiddle/8634b729a50b816762f78aa545fbd72f to your computer and use it in GitHub Desktop.
Some things we learned in the first few months of using React

Basic React Gotchas

Some things we learned in the first few months of using React (2017).

Note: This document might not be easy for complete beginners. It was originally written for someone who already knew React fairly well, and wanted some suggested talking points for a presentation they were planning to give to beginners. I have added some examples over time, but there may be room for more.

Also note: This document was written before React hooks, when we were using ES6 classes for components. Some of the advice (especially the earlier sections, and some of the error messages) does not really apply when using hooks.

Hooks have a number of new and different gotchas, deserving of their own document!

setState does not apply the update immediately

// Constructor
this.state = { foo: 0 };

// Later
this.setState({ foo: 9 });
console.log(this.state.foo); // => 0
// Surprisingly, foo is not 9!  The update will not be applied until later, when React wants to apply it

// Now let's say, in the same tick, another function wants to increment foo by one
this.setState({ foo: this.state.foo + 1 });
// Surprisingly, foo won't be 10 like we expected, even if we wait for the updates to be applied
// foo will be 2, because the second update `{foo: 0 + 1}` overwrote the first update `{foo: 9}`

// Solution: Pass a callback function for the second update, which reads the state at that moment
this.setState(prevState => ({ foo: prevState.foo + 1 }));

eslint-config-airbnb will warn you about this. Although it is a bit over-strict in some areas, I recommend using that eslint config, and disabling any unwanted rules that slow you down.

This above problem is only really an issue if you update the same state property multiple times in one tick. (Like above, when we updated foo twice.) In many cases, we only call setState() once per tick, or we call it twice but with different properties. In those cases, using the prevState solution could be considered overkill, but it could also be considered good practice.

Know when to use an arrow function (or bind)

If a component method uses this in its body, then you might need to make it an arrow function (or a bind). When? Well it’s not about the function itself, it’s about how the other parts of the code call the function.

If they call this.func() or foo.func() then you don’t need to worry about binding func.

But if they pass the function without calling it, e.g. onClick={this.func} then it needs to be arrowed-or-bound. (Because passing a function like that “loses” the association with this. Because Javascript is weird.)

Here is an example of the problem:

  handleChange(event) {
    this.setState({ value: event.target.value });
  }
  
  render() {
    return <input onChange={this.handleChange} />;
  }

The code above will produce the error:

this.setState is not a function

because inside handleChange, the 'this' has been lost. That is because when we passed the handleChange function to onChange, we did not pass its context object. Later on onChange will call handleChange(_) because it cannot call foo.handleChange(_).

Here are three possible solutions:

  // 1. Make handleChange an arrow function, so 'this' will be lexically bound
  handleChange = (event) => {
    this.setState({ value: event.target.value });
  }

  // 2. Bind handleChange in the constructor
  constructor() {
    this.handleChange = this.handleChange.bind(this);
  }

  // 3. Create a new function inside the render method
  //    By using this form, when handleChange is called, it will get the correct this
  render() {
    return <input onChange={(event) => this.handleChange(event)} />;
  }

My team usually opts for solution number 1 because it is just a small change. Solution number 2 is quite traditional, because it could be used before arrow functions were widely available, but it requires an extra line in a different place in the code. Solution number 3 keeps the problem where it is created, but it is inefficient, because it create a new arrow function every time render is called. With hooks option 3 is the only one available. Don't worry about peformance prematurely. Better to wait until you actually observe a performance issue, and profile it.

Solution number 1 has some disadvantages for performance and for inheritance, but I consider these to be minor (and still better performance than solution 3). You may choose which solution to use depending on your team's priorities.

Error: Component changed from unhandled to handled

Caveat: I have paraphrased these error messages. Sometime I should look up the exact wording!

When we see this error, it usually means that we have a form element like this one:

<input ... value={this.state.foo} />

but we forgot to set an initial value for foo in the constructor:

this.state = {
  foo: ''   // should fix it
};

Error: Array item expects keys attribute

Whenever you return an array of components in JSX (e.g. by mapping over an array of items), React needs a unique key for each component, so it can keep track if one item is removed from or added to the middle of the array.

For arrays whose contents are likely to change, the key should not be the array index, but something unique to that item, e.g. the item's id or its name.

// Undesirable (but acceptable if the array is static)
{inventory.map((item, i) => (
  <InventoryItem key={i} weight={item.weight} />
)}

// Good
{inventory.map((item) => (
  <InventoryItem key={item.id} weight={item.weight} />
)}

Error: Can't call setState on an unmounted component.

The full error message:

Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.

This can happen if setState (or other interaction) is performed on a component after the component is no longer being displayed (has been unmounted). This usually involved async code.

For example, if you trigger an asynchronous call to the back-end, but then immediately click away to a different page, the component will disappear before the server response reaches the browser. When you do receive the response, if you try to setState on the unmounted component, then you will get this error in development mode. This case is somewat harmless, and you can disregard it. I believe you can get away with this, because it is a one-off. It does not indicate a memory leak, since the leak will end when the last response returns.

A worse case is if you start an interval or timer loop when a component is first created/mounted, but forget to stop the timer when the component is unmounted. In this case, your interval might keep trying to update the unmounted component forever. This is a memory leak!

In both situations, the solution is to define a componentWillUnmount method, which can clean up all operations (stop the timers, cancel any ongoing API requests).

// Situation
componentDidMount() {
  // Start a scheduled task
  this.intervalId = setInterval(() => {
    this.setState(prevState => ({ counter: prevState.counter + 1 }));
  }, 1000);
}

// Solution: Remember to stop the task when the component is unmounted
componentWillUnmount() {
  clearInterval(this.intervalId);
}

// Optional solution for the other case: Prevent the API response from triggering the handler callback
componentWillUnmount() {
  this.apiRequestPromise.cancel();
}

Learn how to use {children}. It is quite easy, and good to know

It is easy to create a container component with specific behaviour, but it can contain completely different children whenever it is used.

(So for example, all your forms could have a common look, even if what appears inside the forms is quite different.)

<CuteForm onSubmit={...}>
  <input type="text" name="example" value="Example" />
</CuteForm>

const CuteForm = (props) => {
  return (
    {/* We can put common styling and behaviour for forms on this div */}
    <div>
      {/* This adds all the form inputs we were passed by the parent */}
      {props.children}
      {/* All forms need a submit button, so we can put that here, once for all forms */}
      <button type="submit" onClick={props.onSubmit}/>
    </div>
  );
};

Reading the current page URL (with react-router)

Avoid using document.location to determine which page you are on. If the user navigates to a different page, there is no guarantee that your component will be rerendered, so it might not see the location change.

The React Router way is to wrap your component in a <Route path="..." component={...} />

The component referenced in the Route will have a prop location passed to it (and also a prop match with a useful match.params). And, crucially, the component will be re-rendered if the location changes.

Avoid putting too much into state

For clarity, I prefer to keep the fewest things possible in state, and only things which might change.

Some developers are tempted to put other things in state, for example copying props in there, or storing runtime values which could have been derived from existing state.

If a value can be derived from existing state, then I prefer to derive it in the render() function (or in a helper function). Even though that might be less efficient, it keeps the code cleaner.

Optimisation can come later, when the project is more mature.

Performance: Import the smallest import you can

If your library allows it, then do:

import foo from 'lib/foo';

instead of

import { foo } from 'lib';

This can help to keep bundle size smaller. If the library also has bar and baz exports, but you aren’t using them, then the first way will avoid pulling those into the bundle.

(This is somewhat dependent on your webpack config, so depending on your setup you might not need to do this. I don’t know how React Native / Expo behave in this case. I guess it’s more of an issue for web than for mobile anyway.)

With some libraries, such as MUI this style of import can stop VSCode from slowing down.

Performance: Avoid creating new functions each time render() is called. That can break PureComponents.

// Undesirable (gives the button a new function on every render)
<Button onClick={() => this.toggleCheckbox('foo')}>

// Good (always passes the same prop)
<Button onClick={this.toggleFooCheckbox}>

// But yes you might have to declare an extra method in your component
toggleFooCheckbox = () => {
  this.toggleCheckbox('foo');
};

Having said that, if it is difficult to do this, then don’t worry too much. Only do it if it is easy, or you are going to have a lot of these.

Performance: When displaying lots of items on screen, make those items into PureComponents

A pure component only rerenders if its props or state has changed. So pure components are more efficient. (A simple log in the render function can show you how often a render function is called.)

For example, if a parent component keeps rerendering, and it has lots of children which don't always change, it would be inefficient to rerender all the children each time. Make the children PureComponents, and they won't rerender if their props and state have not changed.

If you use redux’s connect() then that already does this for you, so you don't need to use PureComponent. Similary, all pure functions act like pure components (I think).

Avoid putting too many things into one component

This is not really React specific, but a general coding principle.

If a component is handling two concerns, especially two unrelated bits of state for the different concerns, then try to move one of the concerns into a child component.

This avoids unrelated things getting tangled together. One concern per component makes code easier to read and refactor.

Make components for the most common UI elements, even if they are trivial

This is not really React specific either.

If you find yourself using the same pattern in many places, then make a component for it.

For example, in React (and in Jade, Angular, …) I like to make a button library. Whenever I need an “OK” button or a “Cancel” button, I get it from the library.

Then later, if I want to change how Cancel buttons look, it is really easy to change all of the Cancel buttons at once.

(This doesn’t only apply to small components. You can also make a Table component, a Page component, a Popup component and a Form component.)

This idea can also be used to address common layout concerns with layout components.

Build your app out of components, not code

The React-specific advice here is that components are the fundamental building blocks of the application UI, as opposed to say functions or methods. For example:

// If you are rendering a car, and you want to render the badge on the front grille,
// you could call a method in the current class
<Grille>
  {this.renderBadge()}
</Grille>

renderBadge() {
  return <div style={shinyStyle}>{this.props.modelName}</div>;
}

// Or you could call an external function, passing it any data it needs
<Grille>
  {renderBadge({ modelName: this.props.modelName })}
</Grille>

// But it might be better to do it with a new component instead
<Grille>
  <Badge modelName={this.props.modelName}/>
</Grille>

const Badge = ({ modelName }) => (
  <div style={shinyStyle}>{modelName}</div>
);

The advantage here is that the Badge component does not have unnecessary access to all the props and state of the car. It has only the data it needs, in this case the modelName of the car.

This separates the concerns, making it easier to understand and refactor the code in future. It also makes the Badge component trivial to reuse, if that is ever needed in future.

More React bugs

You may or may not need to know about these open issues with React.

See also

A few more gotchas have been written up here by John Kennedy: https://www.codementor.io/johnkennedy/common-react-errors-s662o6gv9

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