Technology16 minute read

React Tutorial: Components, Hooks, and Performance

In recent years, React has established itself as the most popular library in the JavaScript universe. However, rather than rest on its laurels, the React team is still working hard to make it better.

In the second part of our React tutorial series, Toptal JavaScript Developer Kamil Jakubczak takes a closer look at some of its features, including hooks, the latest addition to React.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

In recent years, React has established itself as the most popular library in the JavaScript universe. However, rather than rest on its laurels, the React team is still working hard to make it better.

In the second part of our React tutorial series, Toptal JavaScript Developer Kamil Jakubczak takes a closer look at some of its features, including hooks, the latest addition to React.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Kamil Jakubczak
Verified Expert in Engineering
10 Years of Experience

Kamil is a React Native, JavaScript, and .NET fan who has recently been focusing on React Native. He is also experienced in recruiting.

Read More
Share

As was pointed out in the first part of our React tutorial, getting started with React is relatively easy. Start by using Create React App (CRA), init a new project, and start developing. Sadly, over time, you might hit a situation where your code will become rather hard to maintain, especially if you are new to React. Components may get unnecessarily big or you could end up with elements that could be components but aren’t, so you can end up writing repetitive code here and there.

That’s where you should try to really start your React journey—by starting to think in React development solutions.

Whenever you approach a new app, a new design that you need transform into a React application later on, first try to decide what components will be in your sketch, how you can separate the sketch to make them easier to manage, and which elements are repetitive (or their behavior, at least). Try to avoid adding code that might be “useful in the future”—it might be tempting, but that future might never come and you will keep that extra generic function/component that has plenty of configurable options.

React tutorial: illustration of React components

Also, if a component is longer than, let’s say 2-3 window height, maybe it is worth separating (if possible)—as it will be easier to read it later on.

Controlled vs. Uncontrolled Components in React

In most applications, there is a need for input and some form of interaction with users, allowing them to type something, upload a file, select a field, and so on. React deals with user interaction it in two distinct ways—controlled and uncontrolled components.

The value of controlled components, as their name suggests, is controlled by React by providing a value to the element that interacts with the user, while uncontrolled elements do not get a value property. Thanks to that, we have a single source of truth which happens to be the React state, so there is no mismatch between what we are seeing on the screen and what we currently have in our state. The developer needs to pass a function that will respond to user interaction with a form, which will change its state.

class ControlledInput extends React.Component {
 state = {
   value: ""
 };

 onChange = (e) => this.setState({ value: e.target.value });

 render() {
   return (
     <input value={this.state.value} onChange={this.onChange}/>
   );
 }
}

In uncontrolled React components, we do not care about how the value changes, but if we want to know the exact value, we simply access it through ref.

class UncontrolledInput extends React.Component {
 input = React.createRef();

 getValue = () => {
   console.log(this.input.current.value);
 };

 render() {
   return (
     <input ref={this.input}/>
   );
 }
}

So which should be used when? I would say that controlled components are the way to go in most cases, but there are some exceptions. For example, one case where you need to use uncontrolled components in React is the file type input, as its value is read-only and cannot be programmatically set (user interaction is required). Also, I find controlled components easier to read and easier to work with. Doing validation for controlled components is based on the rerender, the state can be changed, and we can easily indicate that there is something wrong with the input (e.g., format or being empty).

Refs

We already mentioned refs, which are a special feature that was available in class components until hooks appeared in 16.8.

Refs can give the developer access to a React component or DOM element (depending on the type where we attach ref) through reference. It is considered a good practice to try to avoid them and use them only in must-have scenarios, as they make the code a bit harder to read and break the top-to-bottom data flow. Yet, there are cases where they are necessary, especially on DOM elements (e.g., changing focus programmatically). When attaching to a React component element, you can freely use methods from within that component that you are referring to. Still, this practice should be avoided as there are better ways to deal with it (e.g., lifting state up and moving functions to parent components).

Refs also have three different ways they can be accomplished:

  • Using a string literal (legacy and should be avoided),
  • Using a callback function that is being set in the ref attribute,
  • By creating ref as React.createRef() and binding it to a class property and accessing it through it (be aware that references will be available from the componentDidMount lifecycle).

Finally, there are cases when refs aren’t passed down and times when you want to access a deeper reference element from the current component (e.g., you have a <Button> component that has an inside <input> DOM element and right now you are in a <Row> component, and from the row component you want to have access to input the DOM focus function. That’s where you would use forwardRef).

One case where reference is not being passed down is when there is a higher order component being used on a component—the reason is quite understandable as ref is NOT a prop (similar to key) so it isn’t being passed down so it will be referencing the HOC instead of the component being wrapped by it. In such a case, we can use React.forwardRef which takes props and refs as arguments, which can be then assigned to prop and passed down to the component that we want to access.

function withNewReference(Component) {
 class Hoc extends React.Component {
   render() {
     const {forwardedRef, ...props} = this.props;

     return <Component ref={forwardedRef} {...props}/>;
   }
 }

 return React.forwardRef((props, ref) => {
   return <Hoc {...props} forwardedRef={ref} />;
 });
}

Error Boundaries

The more complex things get, the higher the probability that something will go wrong. That’s why error boundaries are a part of React. So how do they work?

If something goes wrong and there’s no error boundary as its parent, it will result in the whole React app failing. It is better to not display information rather than mislead users and display wrong information, but it doesn’t necessarily mean that you should crash the whole application and show a white screen. With error boundaries, you have an added degree of flexibility that you can use. You can either use one across the whole app and display an error message, or use it in some widgets and simply not display them, or display a small amount of information in place of those widgets instead.

Remember that it is only about issues with declarative code rather than imperative code that you write for handling some events or calls. For these, you should still use the regular try/catch approach.

Error boundaries are also a place where you can send information to the Error Logger that you use (in the componentDidCatch lifecycle method).

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    logToErrorLogger(error, info);
  }

  render() {
    if (this.state.hasError) {
      return <div>Help, something went wrong.</div>;
    }

    return this.props.children; 
  }
}

Higher Order Components

Higher Order Components (HOC) are often mentioned in React and are a very popular pattern, one that you will probably use (or already did so). If you’re familiar with HOCs, you’ve probably seen withNavigation, connect, withRouter in many libraries.

HOCs are just functions that take a component as an argument and will return a new component with extended capabilities compared to the one without the HOC wrapper. Thanks to that, you can achieve some easily extensible functions that could enhance your components (e.g., access to navigation). HOCs can also take a few forms called depending on what we have, the only argument always required being a component, but it can take extra arguments—some options, or like in connect, you first invoke a function with configurations that later returns a function which takes an argument component and returns HOC.

There are a few things that you could add and should avoid:

  • Add a display name for your wrapper HOC function (so you know that it is, in fact, a HOC by changing your HOC component display name).
  • Do not use HOC inside a render method—you should already be using an enhanced component inside of it, instead of creating a new HOC component there due remounting it all the time and losing its current state.
  • Static methods are not copied over, so if you want to have some static methods inside of your newly created HOC, you need to copy them over yourself.
  • The mentioned Refs are not passed, so use React.forwardRef as previously mentioned for solving such issues.
export function importantHoc() {
   return (Component) => class extends React.Component {
       importantFunction = () => {
           console.log("Very Important Function");
       };

       render() {
           return (
               <Component
                   {...this.props}
                   importantFunction={this.importantFunction}
               />
           );
       }
   };
}

Styling

Styling is not necessarily related to React itself but it is worth mentioning for a number of reasons.

First of all, the regular CSS/inline styles apply here as normal and you can simply add class names from CSS in the className attribute, and it will work correctly. The inline styling is a bit different than the regular HTML styling. The string is not being passed down with styles but rather objects with correct values for each. Style attributes are also camelCased, so border-radius becomes borderRadius and so on.

React seems to have popularized a few solutions that became commonplace not only in React, such as CSS modules that were recently integrated within CRA, where you can simply import name.modules.css and use its classes like properties to style your component (some IDEs, e.g., WebStorm, also have Autocomplete for that, which tells you what names are available).

Another solution that is also popular in React is CSS-in-JS (e.g., the emotion library). Just to point out again, CSS modules and emotion (or CSS-in-JS in general) are not limited to React.

Hooks in React

Hooks are quite possibly the most eagerly awaited addition to React since the rewrite. Does the product live up to the hype? From my perspective, yes, as they really are a great feature. They are essentially functions that open up new opportunities, such as:

  • Allows the removal of a lot of class components that we only used because we couldn’t have, e.g., a local state or ref, so the code for a component looks easier to read.
  • Enables you to use less code for the same effect.
  • Makes functions way easier to think about and test, e.g., by using the react-testing-library.
  • Can also take parameters, and the result of one can be easily be used by another hook (e.g., setState from useState in useEffect).
  • Minifies way better than classes, which tend to be a bit more problematic for minifiers.
  • Might remove HOCs and render props patterns in your app which introduced new problems despite having been designed to solve others.
  • Capable of being custom-built by any skilled React developer.

There are few React hooks that are included as default. The three basic ones are useState, useEffect, and useContext. There are also several additional ones, e.g., useRef and useMemo, but for now, we shall focus on the basics.

Let’s take a look at useState, and let’s use it to create an example of a straightforward counter. How does it work? Well, basically, the whole construct is really straightforward and looks like:

export function Counter() {
 const [counter, setCounter] = React.useState(0);

 return (
   <div>
     {counter}
     <button onClick={() => setCounter(counter + 1)}>+</button>
   </div>
 );
};

It is invoked with initialState (value) and returns an array with two elements with it. Thanks to array destructuring assignment, we can assign the variables to these elements right away. The first one is always the last state after updates, while the other one is a function that we will use to update the value. Seems rather easy, doesn’t it?

Also, due to the fact that such components used to be called stateless functional components, such a name is not appropriate anymore, because they can have a state as it is shown above. Hence, the names class components and function components seem to be more in line with what they actually do, at least from 16.8.0.

The update function (in our case setCounter), can also be used as a function which will take the previous value as an argument in the following form:

<button onClick={() => setCounter(prevCounter => prevCounter + 1)}>+</button>
<button onClick={() => setCounter(prevCounter => prevCounter - 1)}>-</button>

However, unlike the this.setState class component which was doing a shallow merge, setting the function (setCounter in our case) is overriding the whole state instead.

In addition, initialState can also be a function, not just a plain value. This has its own benefits, as that function will be run only during the initial render of the component and after that, it won’t be invoked anymore.

const [counter, setCounter] = useState(() =>  calculateComplexInitialValue());

Finally, if we are going to use setCounter with the exact same value that we had at the very same moment in the current state (counter), then component will not rerender.

On the other hand, useEffect is about adding side effects to our functional component, be it subscriptions, API calls, timers, or just about anything that we may find useful. Any function that we will pass to useEffect will be run after render, and it will be doing so after every render unless we add a limitation concerning what properties’ changes should be rerun as a second argument of the function. If we want to run it only on mount and clean up on unmount, then we just need to pass an empty array in it.

const fetchApi = async () => {
 const value = await fetch("https://jsonplaceholder.typicode.com/todos/1");
 console.log(await value.json());
};

export function Counter() {
 const [counter, setCounter] = useState(0);
 useEffect(() => {
   fetchApi();
 }, []);


 return (
   <div>
     {counter}
     <button onClick={() => setCounter(prevCounter => prevCounter + 1)}>+</button>
     <button onClick={() => setCounter(prevCounter => prevCounter - 1)}>-</button>
   </div>
 );
};

The above code will be only run once due to an empty array as a second argument. Basically, it is something like componentDidMount in such case, but it fires a bit later. If you want to have a similar hook that is called before browser paint, use useLayoutEffect, but these updates will be applied synchronously, unlike useEffect.

useContext seems to be the easiest to understand, as one provides to which context we want to get access (an object that was returned by the createContext function) and in return, it provides us with the value for that context.

const context = useContext(Context);

Finally, to write your own hook, you can just write something like the following:

function useWindowWidth() {
 let [windowWidth, setWindowWidth] = useState(window.innerWidth);

 function handleResize() {
   setWindowWidth(window.innerWidth);
 }

 useEffect(() => {
   window.addEventListener('resize', handleResize);
   return () => window.removeEventListener('resize', handleResize);
 }, []);

 return windowWidth;
}

Basically, we are using the regular useState hook for which we are assigning as initial value window width. Then in useEffect, we are adding a listener which will trigger handleResize on each window resize. We also clear after component will be unmounted (look at the return in useEffect). Easy?

Note: The word use in all hooks is important. It’s used because it allows React to check if you are not doing something bad, e.g., call hooks from regular JS functions.

Checking Types

React had its own props checking, before Flow and TypeScript were an option.

PropTypes checks if the properties (props) that are received by a React component and checks if they’re in line with what we have. Whenever a different situation occurs (e.g., object instead of an array), we will get a warning in the console. It is important to note that PropTypes are only checked in development mode due to their impact on performance and the aforementioned console warning.

As of React 15.5, PropTypes are in a different package that needs to be installed separately. They are declared along properties in a static property called propTypes (surprise), combining them with defaultProps which are used if the properties are undefined (undefined is the only case). DefaultProps are not related to PropTypes, but they can solve some warnings that might appear due to PropTypes.

The other two options are Flow and TypeScript, and they are way more popular nowadays (especially TypeScript).

  • TypeScript is a typed superset of JavaScript, developed by Microsoft, that can check errors before an app is even running and provides superior autocomplete functionality for development. It also greatly improves refactoring. Due to Microsoft support, which has extensive experience with typed languages, it is a rather safe choice as well.
  • Flow is not a language, unlike TypeScript. It is a static type checker for JavaScript, so it is more akin to a tool that you include in JavaScript than a language. The whole idea behind Flow is quite similar to what TypeScript offers. It allows you to add types so it is less likely to have any bugs before you run the code. Just like TypeScript, Flow is now supported in CRA (Create React App) from the start.

Personally, I find TypeScript faster (practically instantaneous), especially in autocomplete, which seems a bit slower with Flow. It’s worth noting that IDEs such as WebStorm, which I personally use, employ a CLI for integration with Flow. However, it seems even easier to integrate optional use in files, where you simply add // @flow at the beginning of the file to start type checking. Also, from what I can tell, it seems that TypeScript won the battle versus Flow in the end—it is way more popular now, and some of the most popular libraries are getting refactored from Flow to TypeScript.

There are a few more options, mentioned also in official documentation, such as Reason (developed by Facebook and gaining popularity in the React community), Kotlin (a language developed by JetBrains) and more.

Obviously, for front-end developers, the easiest approach would be to hop in and start using Flow and TypeScript, rather than switch to Kotlin or F#. However, for back-end developers that are transitioning to front-end, these might actually be easier to get started with.

Production and React Performance

The most basic and obvious change that you need to do for production mode is to switch to “production” for DefinePlugin and add UglifyJsPlugin in the case of Webpack. In the case of CRA, it is as simple as using npm run build (which will be running react-scripts build). Be aware that Webpack and CRA are not the only options, as you can use other build tools like Brunch. This is usually covered in official documentation, be it official React documentation or documentation for a specific tool. To make sure that the mode is set correctly, you can use React Developer Tools, which will give you an indication of what kind of build you are using (production vs. development). The aforementioned steps will make your application run without checks and warnings that come from React and the bundle itself will be minimized, too.

There are a few more things you can do for your React app. What do you do with the JS file that is built? You can start with just “bundle.js” if the size is relatively small, or maybe do something like “vendor + bundle” or maybe “vendor + smallest required part + import things when they are needed.” This is useful when you are dealing with a very big app and you do not need to import everything at the very beginning. Be aware that bundling some JavaScript code in the main bundle that is not even being used will simply increase the bundle size and make the app load slower at the very beginning.

Vendor bundles can be useful if you are planning on freezing libraries’ versions upon realizing they might not change for a long time (if ever). Also, bigger files are better at gzipping so the benefit that you get from separating might sometimes not be worth it. It depends on the file size and sometimes you will simply need to try it yourself.

Code Splitting

Code splitting can appear in more ways than suggested here, but let’s focus on what we have available in CRA and React itself. Basically, in order to split code into different chunks, we can use import() which works thanks to Webpack (import itself is a proposal in Stage 3 as of now, so it is not part of the language standard just yet). Whenever Webpack sees import, it will know that it needs to start code splitting at this stage and cannot include it in the main bundle (the code that it is inside the import).

Now we can connect it with React.lazy() which requires import() with a file path containing the component that needs to be rendered in that place. Next, we can use React.suspense() which will display a different component in that place until the imported component is loaded. One might wonder; if we are importing a single component, then why we would need it?

That’s not quite the case, as React.lazy() will be showing the component we import(), but import() might fetch a bigger chunk than that single component. For example, that particular component might have other libraries in tow, more code, etc., so one file isn’t necessary—it might be many more files bundled together. Finally, we can wrap all of that in ErrorBoundary (you can find the code in our section on error boundaries) which will serve as a fallback if something fails with the component we wanted to import (e.g., if there’s a network error).

import ErrorBoundary from './ErrorBoundary';

const ComponentOne = React.lazy(() => import('./ComponentOne'));

function MyComponent() {
   return (
       <ErrorBoundary>
           <React.Suspense fallback={<div>Loading...</div>}>
               <ComponentOne/>
           </React.Suspense>
       </ErrorBoundary>
   );
}

This is a basic example, but you can obviously do more. You can use import and React.lazy for dynamic route splitting (e.g., admin vs. regular user, or just really big paths that bring a lot). Be aware that React.lazy only supports default exports as of now and doesn’t support server-side rendering.

React Code Performance

Regarding performance, if your React application is acting laggy, there are two tools that can help you figure out the issue.

The first one is Chrome Performance Tab, which will tell you what happens with each component (e.g., mount, update). Thanks to that, you should be able to identify which component is exhibiting performance issues trouble and then optimize it.

The other option is to use DevTools Profiler which became available in React 16.5+, and with cooperation from shouldComponentUpdate (or PureComponent, which was explained in part one of this tutorial), we can improve performance for some critical components.

Obviously, employing basic best practices for the web is optimal, such as debouncing some events (e.g., scroll), being cautious with animations (using transform instead of changing the height and animating it) and so on. Using best practices can be overlooked very easily, especially if you are just getting to grips with React.

The State of React in 2019 and Beyond

If we were to discuss the future of React, personally, I wouldn’t be too concerned. From my perspective, React will have no trouble keeping its throne in 2019 and beyond.

React has such a strong standing, backed by a large community following, that it will be difficult to dethrone. The React Community is great, it isn’t running out of ideas, and the core team is constantly working to improve React, adding new features and fixing old issues. React is also backed by a huge company, but the licensing problems are gone—it is MIT licensed right now.

Yes, there are a few things that are expected to be changed or improved; for instance, making React a bit smaller (one of the measures that were mentioned is removing synthetic events) or renaming className to class. Of course, even these seemingly minor changes could cause issues such as impacting browser compatibility. Personally, I also wonder what will happen when WebComponent gains more popularity, as it might augment some of the things that React often is used with today. I do not believe that they will be an outright replacement, but I believe they might complement each other nicely.

As for the short-term, hooks just came to React. That is probably biggest change since the rewrite happened in React, as they will open up plenty of possibilities and enhance further function components (and they are really being hyped up now).

Lastly, as that’s what I’ve been doing most recently, there’s React Native. For me, it is a great technology that changed so much over the past couple of years (lack of a react-native link was probably the biggest issue for most people, and there were obviously plenty of bugs). React Native is getting its core rewritten, and that should be done in a similar way to the React rewrite (it is all internal, nothing or almost nothing should be changed for developers). Async rendering, a faster and more lightweight bridge between native and JavaScript, and more.

There is plenty to look forward to in the React ecosystem, but hooks (and React Native if someone loves mobiles) updates are probably the most important changes we will see in 2019.

Understanding the basics

  • What are React hooks?

    Hooks are functions that let you “hook into” React state and lifecycle features from function components.

  • What's something worth remembering about them?

    They should start with “use” and should be called only from React component functions. You can also create your own hooks!

  • Do uncontrolled components make sense?

    In some situations, yes. In file uploading, they are necessary, but in most cases, controlled components are easier to work with.

  • Can all of this be used in React Native?

    Mostly. First of all, performance related to splitting isn’t there in RN (it can be done but not out of the box—it needs to be rather hacked around as Metro doesn’t work with such stuff yet). Everything else applies to RN.

  • Which ref should I use?

    DO NOT use the legacy one. The preferred method is to use createRef, but callback should be just fine.

  • Hooks, HOC, or render props?

    If something can be solved with hooks, I would use hooks. They are way easier (at least for me) to work with as they seem to be easier to share with different components and they do not bring a pyramid of doom in the case of render props or the “where this is coming from” thing in HOCs.

  • Why were hooks so anticipated?

    They can solve some issues with code sharing that we had previously with using HOCs and render props patterns that sometimes could make a code a bit complicated without reason.

Hire a Toptal expert on this topic.
Hire Now
Kamil Jakubczak

Kamil Jakubczak

Verified Expert in Engineering
10 Years of Experience

Poznań, Poland

Member since September 18, 2017

About the author

Kamil is a React Native, JavaScript, and .NET fan who has recently been focusing on React Native. He is also experienced in recruiting.

Read More
authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.