Forget Everything you Learned about React – Hooks Rock!


Full-Stack Developer

React-Hooks

Twenty years ago I worked for a bank converting Clipper 5 code into Visual Basic as part of Y2K migrations.  I was young and impressionable and I remember my manager said to me, “If something is getting too complex, there’s probably a better way!”

Complexity can be a significant barrier for developers considering a new technology. We are often pressed for time and we need to be confident that the learning curve is surmountable within a given time-frame to be worth the investment.

I am relatively new to React js, and during my time at NearForm I’ve seen some common problems being solved in various ways, from mixins, to higher-order components, to render props.  Learning React can be daunting enough for someone new to the technology. Discovering that there are various techniques that must be grasped in order to solve more complex problems can create a significant barrier.

These techniques came about organically over time and developers who used React from the get-go have had time to absorb why these are necessary but they can cause difficulty for someone learning the latest incarnation from scratch.

What are React Hooks?

React Hooks are an exciting, non-breaking addition to React js that facilitate improved functional decomposition of components and re-use of code. Custom hooks allow related slices of stateful components to be extracted into reusable libraries that can be elegantly imported into functional components. This has major implications for the simplifying, testing, organization, sharing, and maintenance of potentially simpler, and more atomic code.

The React hurdles for new developers:

    • Choosing whether to use a function or a class to create a React component. Some developers just use classes because of the fear that it will need to be converted to a component later. However, classes can get more difficult for humans and machines to analyse and minify and can be difficult to share stateful logic between. https://reactjs.org/docs/hooks-intro.html#motivation
    • Using render props we can create reusable React components that take a function as a property, which returns a react element instead of implementing its own render logic. For example, downshift from PayPal can be used with front end presentation components like Material-UI. The issue here is that code can begin to get untidy with lots of nesting and it can be difficult to analyse.
    • Component lifecycle methods such as “componentDidMount”, “componentDidUpdate”, “componentWillUnmount” can be used to register and unregister from browser events and update state using props. This can get very confusing, related pieces of code can end up being separated.

Has React js suffered from getting too complex and could there be a better way that addresses these issues?

The developers at Facebook/React certainly think so with the addition of “React hooks’ to the framework, which seems to solve practically all of the complexities above in one fell swoop.  React hooks are currently still under alpha, but will be added to version 16 of React js over the next few months as it will be a non-breaking change.

To try hooks out create a standard react app using npm:

npx create-react-app [your-app-name]

(npx is not a typo)

And then change to the root of the newly created app and run:

npm install react@next react-dom@next

(by the time you read this it may be live)

I’m already Hooked!

React components with stateful logic can now also be created as functions!!!

The first thing to realise about React hooks is that there are several built-in functions that work out of the box such as the useState and useEffect hooks.  These can be used directly or to create custom hooks. These two are the bread and butter of hooks, and go together like a horse and carriage, Starsky and Hutch, Batman and Robin… (you get the idea). Using them means that there is no longer a requirement to use classes in order to create a component that requires state/lifecycle management, we can just use functions instead.  

The intention here is that, in future, there should be just one recommended, consistent way to create components.  Functions are an obvious choice and using them with hooks has positive impacts on diagnostics and debugging, minification, lines of code, code reuse, and complexity reduction.  Don’t worry if you use classes extensively, they will always remain part of React; this is just a recommendation for future components developed.

Another thing worth mentioning is that this is not a breaking change to React js, it’s a light addition that might have huge implications…

“Great oaks from little acorns grow”

React Hooks Examples

The following short React tutorial, should help readers learn some React hooks basics.  Let’s keep the code fairly straightforward and create a Form component using a function. The function will manage the Form field elements using custom hooks to manage state and events for modifying state and validation.  We’ll also use another custom hook later to manage form submission.

We’ll use Material UI for our front end components and theming, so you’ll need to install that in your app first.

From your terminal install material-ui core and styles using the following commands

npm install @material-ui/core

(the main material ui library, theming, components etc.)

npm install @material-ui/styles

(contains the new custom hooks for theming support. Note: to switch from the default style to this version, currently you must import install from this library, and execute it before importing other material-ui components.)

Then modify App.js as follows:

import React from "react";
import { createMuiTheme } from "@material-ui/core/styles";
import { ThemeProvider } from "@material-ui/styles";

// create our material ui theme using up to date typography variables
const theme = createMuiTheme({
 typography: {
   useNextVariants: true
 }
});

// Let's convert App from class to function to get into the mood!
function App() {
 return (
   <ThemeProvider theme={theme}>
     { /* our demo form will go here */ }
   </ThemeProvider>
 );
}

export default App;

The above code sets up a new MaterialUI theme object using `createMuiTheme`, which we pass as a prop to ThemeProvider. Now it is accessible to all subcomponents of ThemeProvider.

Next, create a basic Form component called `Form.js` in a `/components` subfolder with the following code (explained below):

// import built in hook for functional component state

import React, { useState } from "react";  
// material-ui hook functions
import { makeStyles, useTheme } from "@material-ui/styles";
// import material-ui components
import { Paper, TextField, Typography } from "@material-ui/core";
//create a custom material-ui hook to access class styles
const useStyles = makeStyles(
 theme => ({
   root: {
     padding: theme.spacing.unit * 3
   }
 }),
 { withTheme: true }
);

function Form() {
 // use of custom hook to bring in styles (usually done with HOC and prop)
 const classes = useStyles();
 // we can also use a hook to access the theme object (as above)
 const theme = useTheme();
 // create and init state variable and state mutator with useState React hook
 const [data, setData] = useState("");

 // create memoised event handler for input field onChange event
 // similar to this.handleChange in class component

 const handleChange = useCallback(e => {
   setData(e.target.value);
 }, []);

 return (
   <Paper className={classes.root}>
     <Typography variant="h4">NearForm Hooks Demo</Typography>
     <Typography variant="body1" color="primary">
       Theme primary color = {theme.palette.primary.main} (obtained from
       useTheme hook)
     </Typography>
     <TextField name="test" value={data} onChange={handleChange} />
     <Typography color="primary">Data: {data}</Typography>
   </Paper>
 );
}
export default Form;
  • The first thing we do here is call `makeStyles` to create a material-ui custom hook and assign it to `useStyles`, which we call in the component function to get a reference to the `classes` object.  We can then use `classes` as normal. The usual way to do this is to wrap a given class component with a HOC (withStyles), and access the `classes` object via props.
  • In this step, we also pass in `withTheme` option, in order to provide access to the `theme` object within the `makeStyles` function.  This allows us to use the `theme` object to define custom styles, in this example, we access the base spacing unit of the theme and multiply by 3.
  • MaterialUI has provided a custom hook called useTheme to get access to the theme within the component.
  • Next, we call useState to set up a state variable called data, and a setter function called setData and we initialise the value of data to empty string with the useState function call parameter. This would not have been possible in a standard functional component, and if using a class we would have had to create and initialise the state object in a constructor, and set the state using this.setState.
  • Next, we create a memoised handler for the onChange event of the TextField component, using another react hook called useCallback, and call setData within the handler to update the state variable, data.  This is similar to using this.handleEvent in a class component.
  • Finally, we render using some material ui components, most notably the TextField component which is bound to the state using the value field, and invokes the handleChange event to set state from onChange.

Now import and render <Form /> within the App.js to try it out.

Hooks, < Lines and Simpler!

One of the most compelling features of hooks is that custom hooks can be created that utilize the built-in react hooks. Remember, they are normal javascript functions, which can take objects or functions as parameters and can return complex objects etc.  and so elegant hooks can be created to manage the state and props of components, separating logic from rendering concerns.

For example, imagine we wanted a hook for an input component that manages its state, it’s validation onChange or onBlur, we could write a hook called useInput as follows:

export function useInput(name, defaultValue) {
 // set up the state for the inputs value prop and set it to the default value
 const [value, setValue] = useState(defaultValue);
 //set up state for the inputs error prop
 const [error, setError] = useState(null);

 // set up the event handler for onChange event
 function handleChange(e) {
   // set the state no matter what
   setValue(e.target.value);
   // cancel any error
   setError(null);
 }

 // set up event handler for onBlur, if value is not set, setError to true
 function handleBlur() {
   if(!value) return 
   setError(true)
 }

 // return object 
 return {
     name,
     value,
     onChange: handleChange,
     onBlur: handleBlur,
     error
 };
}
  • We name the function useInput.  The convention use[CustomHookName] is suggested by the react team when creating custom hooks for linting purposes.
  • The function creates two state variables and corresponding mutators for the components value and error state
  • It then creates handlers for onChange and onBlur, onChange to set the value, and onBlur to validate
  • Finally, we return an object with everything set up

Now if we go back to our render method, we can call our new customHook towards the top of our modules as follow

const test = useInput("test", "");
//We can then apply the custom hook directly to our input control as follows:
<TextField {...test} />

Now the custom hook and TextField are linked together. The state and events are managed within the hook, and the object returned by the hook passes the state and handlers to the TextField object using the spread operator to pass in as props.

The function for providing input state and functionality can be put in a library for future use and the logic is kept completely separate from the visual rendering and is managed in an intuitive way.  

The custom hook can be reused then for multiple inputs if necessary e.g.

const email = useInput("email", "");
const age = useInput("age", "");

<TextField {...email} />
<TextField {...age} />

Another advantage of this is that unit tests can be written effectively around the custom hook function, and the logic for handlers and state are managed in one place in a straightforward way.

Let’s take the example forward a little bit and make it a little more useful, adding in simple validation function and a custom hook for form submit.  Create a /hooks folder and create a file called form.js within with the following evolution of our custom handler code:

import { useState } from "react";

// custom hook for input elements
export function useInput(name, defaultValue, validate, regex) {
 // set up the state for the input item and error
 const [value, setValue] = useState(defaultValue);
 const [error, setError] = useState(null);

 // handle the onChange event
 function handleChange(e) {
   // set the state no matter what
   setValue(e.target.value);
   setError(null); 
 }

 // handle onBlur event
 function handleBlur() {
   handleValidate();
 }

 // call validate if supplied and set error appropriately
 function handleValidate() {
   const valid = validate && validate(value, regex)
   setError(!valid);
   return valid;
 }

 return {
   props: {
     name,
     value,
     onChange: handleChange,
     onBlur: handleBlur,
     error
   },
   validate: handleValidate
 };
}

In the code above, we pass in a reference to a callback function, which takes the value and a regular expression as arguments.  The user must create a validation function in this example but a library of validations could easily be created for common validations or a third party validation library could be used. The returned object also groups props into a sub-object and provides access to `validate` in order to allow an external caller to validate the value represented by the instance of the hook.  We do this to allow the submit button to validate before form submission.

Following is a custom hook for form submission, which takes an array of items returned from then custom input hooks and performs validation across all inputs on submit.  If successful, it will call back to a success callback function passed as a parameter, whereby the containing component might send the data to a server.

export function useSubmit(inputs, success) {
 // set up the state for the inputs causing errors
 const [errorItems, setErrorItems] = useState(null);

 // handle submit
 function handleSubmit(e) {
   e.preventDefault(); //prevent page refresh
   //validate every input (in case there was no blur event)
   const errorItems = inputs.filter(input => !input.validate());
   //persist the error items to state
   setErrorItems(errorItems);
   // if no errors, call success with name, value pairs as parameter
   if (errorItems && errorItems.length === 0) {
     success &&
       success(
         inputs.map(({ props: { name, value } }) => ({
           name,
           value
         }))
       );
   } 
 }

 return {
   props: {
     onSubmit: handleSubmit
   },
   errorItems
 };
}

The main action above happens in handleSubmit:

  • First, we prevent the default action to prevent a page refresh onSubmit
  • We then cycle through the inputs, calling to their individual validation handlers
  • If there are any invalid inputs, these are stored in errorItems state (and returned)
  • If there are no invalid items it calls back to the success call back function passing in the valid data, which would typically send the data to a server for persistence

To complete the demo, consider the final implementation of Form.js

import React, { useState } from "react";

import { makeStyles, useTheme } from "@material-ui/styles";
import {
 Paper,
 Button,
 Typography,
 Table,
 TableRow,
 TableCell,
 TextField,
 TableBody,
 TableHead
} from "@material-ui/core";
import { useInput, useSubmit } from "../hooks/form";

//create a hook for classes objects
const useStyles = makeStyles(
 theme => ({
   root: {
     padding: theme.spacing.unit * 3
   },
   form: {
     marginTop: theme.spacing.unit * 3
   },
   input: {
     marginBottom: theme.spacing.unit * 3
   }
 }),
 { withTheme: true }
);

// regular expression constants for validation 
const validations = {
 // eslint-disable-next-line
 EMAIL: /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
 NUMBER: /^\d*$/
};

function Form() {
 // use of hooks to bring classes style sheet in (usually done with HOC)
 const classes = useStyles();
 // we can also use a hook to access the theme
 const theme = useTheme();

// our custom validation function, which the hook calls back to
function handleValidation(value, regex) {
   // we could get fancy here with validations based on type of input
   // could be put in a form hook library and imported
   if (value && regex && value.match(regex)) return true;
   return false;
 }

 const email = useInput("Email", "", handleValidation, validations.EMAIL);
 const age = useInput("Age", "", handleValidation, validations.NUMBER);
 // the data we're going to submit, just using a standard useState hook to display
 const [data, setData] = useState(null);

 // our success handler when all items are validated
 function handleSuccess(data) {
   // we're just setting the state here, but typically this would
   // be sent to the server for further validation and persistence
   setData(data);
 }

 //the custom hook that is called onSubmit, taking our two input hooks return values
 //as parameters, this means the state from the two inputs is available to this hook
 const submit = useSubmit([email, age], handleSuccess);

 // our render method, which displays our form, text fields with error labels
 // hooked up to the custom hooks, it also renders data that has been successfully
 // validated by the form
 return (
   <Paper className={classes.root}>
     <Typography variant="h4">NearForm Hooks Demo</Typography>
     <Typography variant="body1" color="primary">
       Theme primary color = {theme.palette.primary.main} (obtained from
       useTheme hook)
     </Typography>
     <form className={classes.form} {...submit.props}>

       <TextField label="Email" variant="outlined" {...email.props} />
       {email.props.error && (
         <Typography variant="body1" color="error">
           Invalid Email address
         </Typography>
        )}

       <TextField label="Age" variant="outlined" {...age.props} />
       {age.props.error && (
         <Typography variant="body1" color="error">
           Invalid age
         </Typography>
       )}

       <Button type="submit" color="primary" variant="contained">
         Submit
       </Button>
       {submit.errorItems && submit.errorItems.length > 0 && (
         <Typography variant="body1" color="error">
           {`Please fix ${submit.errorItems && submit.errorItems.length} form
           field error(s)`}
         </Typography>
       )}
     </form>

     {data && (
       <Table>
         <TableHead>
           <TableRow>
             <TableCell>Input Field</TableCell>
             <TableCell>Validated Input</TableCell>
           </TableRow>
         </TableHead>
         <TableBody>
           {data.map((item, index) => (
             <TableRow key={`form-${index}`}>
               <TableCell>{item.name}</TableCell>
               <TableCell>{item.value}</TableCell>
             </TableRow>
           ))}
         </TableBody>
       </Table>
     )}
   </Paper>
 );
}

export default Form;

UseEffect

There are other hooks that are well worth investigating.  I’ll give a brief example of useEffect, which can be used to set up and shut down correctly as part of a component’s lifecycle.  With class based components there are some lifecycle methods, and again this can get confusing as some are deprecated.

Typically, the most commonly used lifecycle methods are componentDidMount, componentDidUpdate and componentWillUnmount to perform actions on addition to the DOM, updates and removal respectively.  Typically things like registering and deregistering from events are done here.

Consider the following custom hook:

import { useState, useEffect } from "react";
export function useClock() {
 // set up our time state variable, mutator and initialize
 const [time, setTime] = useState(new Date().toTimeString().split(" ")[0]);
 // timer callback handler
 function tick() {
   setTime(new Date().toTimeString().split(" ")[0]);
 }

 useEffect(
   () => {
     // set up timer to callback to tick
     // same as componentDidMount
     const timer = setInterval(tick, 50);
     return () => clearInterval(timer); // same as componentWillUnmount
   },
   [] // set up once, i.e. no re-register on update
 );
 return time;
}

A lot is happening in here in just a few lines of code:

  • First, we call useState hook to set up the time state variable and setter function, we also initialise the time variable as a string (hh:mm:ss)
  • We then set up a tick function, which will be our timer callback function on interval expiration
  • Next, we call useEffect, which sets up the timer interval, this is the same as componentDidMount and componentDidUpdate in a standard react class.  The return of this removes the timer and is the same as componentWillUnmount
  • The second argument here [] ensures we only setup and tear down the timer once.
  • Finally, we return the time state object, which we can use in our app to display the time

E.g.

const time = useClock();
return (
<Typography>Current Time: {time}</Typography>
)

As well as giving us the ability to do all of this within a function instead of a class, this really tidies up where code resides keeping the logic in one place for registering and tidying up, as well as managing state related to the hook

This could be used for creating hooks to interact with browser events like resizing, mouse movement or with apis centrally.

E.g. to create a custom hook to capture browser mouse position

function useMousePosition() {
 // set up pos state object, and initialize
 const [pos, setPos] = useState({x: 0, y: 0});
 useEffect(() => {
   // handler updates the state
   const handler = ({ clientX, clientY }) => setPos({ x: clientX, y: clientY });
   // create handler on mount, update
   window.addEventListener("mousemove", handler);
   //perform cleanup
   return () => window.removeEventListener("mousemove", handler);
 }, []);
return pos;
}

Other Hooks

There are a few other hooks in the API including

  • useContext(): which takes a context object as an argument, when the context changes a re-render occurs with the value returned.  This can be very useful for global updates to a site like language changes or currency update, or maybe global filters.
  • useReducer():  this behaves like useState, except the state is passed through a reducer dispatch function, which takes the state and action as parameters (anyone used to redux will understand the benefits of this).
  • useCallback(): creates a memoized function that is only called if inputs change
  • useRef(): used to create an object can be assigned to a ref prop on react components so that we can perform things like setFocus
  • useImperativeMethods(): used with nested refs so parent ref can select correct ref
  • useLayoutEffect(): this is the same as useEffect only it runs synchronously after the DOM is finished updating (favour useEffect).

ESLint Plugin

React have provided an eslint plugin tool to enforce some of the rules required regarding hooks.  The main rule is that they must appear at the top of a module and should not exist within conditional statements because the order of creation is important.

The convention for creating custom hooks is to put the word ‘use’ at the front of your hook function name e.g. useTimer, useWindowPosition, useInput and so on.

To install the eslint plugin, ensure you first have eslint installed and saved as a development dependency

npm install eslint –save-dev

Then install the hooks linting plugin as a developer dependency

npm install eslint-plugin-react-hooks@next –save-dev

Then add the following to your esLink config section of package.json

"eslintConfig": {
   "extends": "react-app", //should be already in present
   "plugins": [
     "react-hooks"
   ],
   "rules": {
     "react-hooks/rules-of-hooks": "error"
   }
 },

Finally, add the following line to your scripts section in package.json

"lint": "eslint src/**/*.js"

Save and you can now run npm lint from the terminal and the hooks rules will be checked.

To try it out and verify if installed correctly, enclose a hook within an if statement and run the lint command.

Observations

I think in the very near future we will see lots of new useful libraries using hooks for many common tasks that were previously cumbersome to separate from component logic.  One that immediately comes to mind is a set of hooks that abstract browser events. Typically, developers register and unregister listeners for common tasks like mouse position, keyboard strokes, window sizing, drag/drop and so on, and knowledge of these browser events is required and often being directly called within multiple components’ lifecycle events.  As per the demo above, a couple of these are implemented as custom hooks and using them is as simple as calling a single function e.g. useResize. All of this logic can now be contained within a custom hook, which could greatly simplify many components. This could be done previously nesting components into HOCs or creating render props etc., but it would become a nesting nightmare, whereas with hooks a developer can simply pull in all the hooks necessary one-by-one at the top of the component making it exceptionally flat, clean and readable.

A concern I have would be for future developers learning the technology.  I think hooks will greatly simplify React development, which is fantastic, however for those new to the technology, it may lull them into a false sense of security in terms of their full understanding of the framework.  For example, if they learn and become comfortable using React in this way and are then given the task to maintain code built using the mechanisms mentioned above, such as mixins, render props, HOCs etc., they may face challenges. This is no reason not to embrace hooks however, it’s just that experienced developers should be understanding when less seasoned react developers assist in the maintenance of such code.

Conclusion

React hooks are a fantastic addition to the React js Framework that will:

  • greatly simplify React components,
  • make code more easily testable,
  • make code more atomic (living up to the logo),
  • reduce complexity,
  • facilitate simpler creation of shareable stateful logic,
  • naturally, organize code that is tightly related (e.g. registering and deregistering from events using useEffect),
  • make component lifecycle easier to manage,
  • make it easier to profile and debug,
  • make it more readable: no more this.state, this.props
  • reduce the number of ways of doing things, making it quicker to learn

Resources and Links

React Hooks Demo Link

React Hooks Demo Github Repo

React Hooks Documentation

Excellent article by Dan Abramov

At NearForm, we have vast experience in building solutions across a broad tech stack to deliver reduced complexities and overcome common hurdles. If you are creating modern applications and leveraging web technologies, contact us to learn more about how we can help.

You might also like some of our previous blog posts on React:

Managing React state with Render Props

Exploring React Portals

Sharing React components with Lerna

 

Image credit: Robson Hatsukami Morgan on Unsplash

Top