Well, hello there hooks!
React Hooks
We have already seen how React operates using class
based components. In this lesson, we will talk about another approach for
creating React components. This approach is often called hooks
History
Prior to the introduction of hooks
React had the concept of stateless
functional components. These are components that do not maintain an internal
state with this.state
and the corresponding this.setState
. The simplest
functional component might be our HelloWorld
component:
class HelloWorld extends React.Component {render() {return <div>Hello, World!</div>}}// <HelloWorld />
In this case, the component does not maintain any state. Each time it is
rendered, it merely uses <div>Hello, World!</div>
as its content. In this way,
we say that it is stateless.
Stateless Functional Components
However, there is overhead in writing a component this simple as an entire
class. We only need and use the render
method. In these cases, we can reduce
this code to just a function with the name HelloWorld
function HelloWorld() {return <div>Hello, World!</div>}// <HelloWorld />
This code is already easier to read and understand. The component HelloWorld
just returns the JSX representing <div>Hello, World</div>
Props for Stateless Functional Components
What about a component that receives properties from a parent component? Without
a class, how do we utilize the this.props
we get from inheriting from
React.Component
?
The answer is quite simple; the functional component receives the properties via
an argument we will call props
function HelloWorld(props) {return <div>Hello, {props.name}!</div>}// <HelloWorld name="Sandy" />
Again, very simple. We do not need to worry about remembering this.props
or
dealing with any of the extra complications of a class!
What about methods to handle events? Again the answer is quite simple. We can declare the functions either inside or outside the component! Remember, functions are just variables themselves.
Handling events in Stateless Functional Components
function handleClickOnDiv(event) {console.log('You clicked on the div!')}function HelloWorld(props) {return <div onClick={handleClickOnDiv}>Hello, {props.name}!</div>}// <HelloWorld name="Sandy" />
We can also define the function inside the method to add some "encapsulation." Our handle methods cannot name collide with functions that other components might need.
function HelloWorld(props) {function handleClickOnDiv(event) {console.log('You clicked on the div!')}return <div onClick={handleClickOnDiv}>Hello, {props.name}!</div>}// <HelloWorld name="Sandy" />
Finally, we may choose to write the handling functions as arrow functions. Given
that we do not need the ability of arrow function to automatically bind this
,
we don't need the arrow syntax. However, some developers prefer to write
handling functions as arrow syntax.
function HelloWorld(props) {const handleClickOnDiv = event => {console.log('You clicked on the div!')}return <div onClick={handleClickOnDiv}>Hello, {props.name}!</div>}// <HelloWorld name="Sandy" />
Functional Components vs Stateful Components
Ok, so these examples demonstrate that writing components as simple functions is more straightforward than writing classes. However, this style was limited to only those that do not maintain a state. This limitation would lead some developers to have one stateful component that only maintained the state and then deferred all rendering to a stateless component.
This might look like:
class ToDoListContainer extends React.Component {state = {list: [],}addItem = item => {this.setState({ list: [...this.state.list, item] })}// code to remove items, sort items, mark complete, etc.render() {return (<ToDoListlist={list}addItem={this.addItem}deleteItem={this.deleteItem}/>)}}function ToDoList(props) {return (<ul><li onClick={props.addItem}>Add item!</li>{list.map(item => (<li key={item.id} onClick={props.deleteItem}>{item}</li>))}</ul>)}
Thus the ToDoList
is still a functional component. It does not maintain a
state. Also, the ToDoListContainer
does not do any rendering, deferring that
to the ToDoList
component.
In this way, we get the best of both worlds, the stateful component only manages state, and the stateless component receives its properties from the outside world and is only concerned with rendering content and dispatching events.
Can We Do Better?
Leading up to the React 16.8.0
release, the React developers wanted to address
issues noticed over five years of writing and supporting React based projects.
Here are the issues these projects would have:
- It is hard to reuse stateful logic between components
Take our ToDoListContainer
example from above. Its real purpose is to manage a
list of added, deleted, sorted, and marked complete items. If we wanted to reuse
this idea for a different domain, say perhaps an entire project, it would not be
straightforward. It will always want to render a ToDoList
. Various approaches
exist to deal with this. For instance, we could send the class name of the
component as one of the props
to ToDoListContainer
-- or we could use the
fact that this.props.children
would contain the children of a component. We
could then return something like return this.props.children
, but with a way to
inject the prop
of the list
.
However, all of these approaches had drawbacks.
- Complex components become hard to understand
We have seen some of the
React Lifecycle when we
used componentDidMount
to find a place to perform tasks such as
loading data into state from an API
However, many other lifecycle methods exist. On large projects, classes would become loaded with extra code to handle many different component requirements. These components often became large and difficult to refactor.
- Classes confuse both people and machines
Classes are relatively new to JavaScript, which, until recently, did not have
native support in most browsers for the syntax. The new nature of classes
meant that any code with class
would have to be
transpiled. It also
makes us have to understand JavaScripts dreaded this
function
s are much simpler to write and to understand. Functions lack the
syntax concept complexity of classes.
Enter Hooks
The idea of hooks
were introduced to allow React developers to do everything a
traditional class
based component could do, but with only using function
style definitions.
React 16.8 was
released on February 6, 2019. So they are very new to the React landscape.
However, since their introduction, they have risen in popularity and are
considered the primary development style going forward. Most third-party
libraries support class
based components but are developing all the latest
features focused on hook
usage.
The Simplest Hook: useState
We said that one of the things that Stateless Functional Components
could not
do is maintain state. Certainly, before hooks
we could not define a function
style component and maintain state at all. This limitation is what useState
attempts to resolve.
Let us make a component that has a straightforward counter that increments its value on pressing a button.
function Counter() {// prettier-ignoreconst counterValueAndSetMethod /* this is an array */ = useState( 0 /* initial state */)const counter = counterValueAndSetMethod[0]const setCounter = counterValueAndSetMethod[1]return (<div><p>The counter is {counter}</p><button onClick={() => setCounter(counter + 1)}>Count!</button></div>)}
Whoa! Let us break this down.
We start the very first line of code with:
const counterValueAndSetMethod = useState(0)
This line of code does a few things. First, it declares that we are going to use
some state. It then says that the state's initial value is going to be the
number 0
.
useState
rules
useState
has a few particular rules that we need to remember:
The value given to
useState
in parenthesis is used as the initial value only the first time the component's instance is rendered. Even if the component is rendered again due to a state change, the state's value isn't reset to the initial value. This behavior may seem strange if we are going to calluseState
again when that render happens. How React makes this happen is a concept deeper than we have time to discuss here.useState
always returns an array with exactly two elements. The first element is the current value of the state and the second element is a function that can change the value of this state
In this way useState
is kind of a mixture of this.state
and this.setState
-- since useState
returns a variable containing the state, and another that
allows us to update the state.
It is different than this.setState
from classes in these ways:
this.setState
in classes allows us to do a partial update. That is we can callthis.setState({ key: value})
and thatkey
andvalue
will be merged into the current state. However, withuseState
the state is replaced with whatever we send it.This is ok due to the second difference. In
class
we would define a single state and have to use an object to track multiple state elements in a single component. Thus you would often see states likestate = { board: [], score: 0, playerOneName: 'Bill', playerTwoName: 'Susan'}
. In hooks, this would be four differentuseState
calls.
Using the useState
return value
The next two lines of code:
const counter = counterValueAndSetMethod[0]const setCounter = counterValueAndSetMethod[1]
Make two local variables to store the current value of our state, which we
call counter
and the method that updates the counter as setCounter
Then in the JSX, we can use those two local variables. The code
<p>The counter is {counter}</p>
will show the current value of the counter.
The code <button onClick={() => setCounter(counter + 1)}>Count!</button>
will
call setCounter
to change the value of the counter, and make it the counter
plus one.
However, this code is not as compact as we can make it! We can use
array destructuring assignment
to simplify the code.
The code:
const counterValueAndSetMethod = useState(0)const counter = counterValueAndSetMethod[0]const setCounter = counterValueAndSetMethod[1]
can be rewritten as such:
const [counter, setCounter] = useState(0)
and is how every example of useState
will appear. See
this article
for more details on how and why this syntax works.
Thus our component will look like this:
function Counter() {const [counter, setCounter] = useState(0)return (<div><p>The counter is {counter}</p><button onClick={() => setCounter(counter + 1)}>Count!</button></div>)}
We have just combined the best of both worlds. We have the simplicity of the function component with the ability to update state!
Adding more state
Let us say we also wanted to keep track of a person's name on the counter. Traditionally we would define a state like this:
class CounterWithName extends React.Component {state = {counter: 0,name: '',}}
However, with hooks
, we will make two independent states that each track a
single piece of information.
Separating these pieces of state has a few benefits:
It is easier to remove one part of the state since it has its own variable and state changing function.
We can more easily tell where in the code a piece of state or a state changing function is used.
We don't have to worry about using
this.state.name
orthis.state.counter
, justcounter
andname
.
Let's see what the component would look like if we were to use hooks:
function CounterWithName() {const [counter, setCounter] = useState(0)const [name, setName] = useState('Susan')return (<div><p>Hi there {name} The counter is {counter}</p><button onClick={() => setCounter(counter + 1)}>Count!</button><p><inputtype="text"value={name}onChange={event => setName(event.target.value)}/></p></div>)}
Demo
Hi there Susan, the counter is 0
Ah, how nice. We have independent variables to track our state, instead of
chained object access (e.g. name
vs this.state.name
) and very simple methods
to update the state setName(event.target.value)
Not everything is perfect in the land of Hooks
A complex component could have a large number of useState
Furthermore, we
could find ourselves tracking many variables and functions. React has another
hook, useReducer,
that can simplify the situation. Many third-party libraries
exist to address managing complex state.
We have not talked about implementing "The first time the component is rendered,
use fetch
to get some data from the internet and update the current state" --
We will see that in a future lesson.
The React team has a nice example of hooks in their guide to hooks