OverReact with Hooks — A hands on session
It all started with a tweet by Eric Elliott and everyone loses their mind.
React has a setState() problem: Asking newbies to use setState() is a recipe for headaches. Advanced users have learned to avoid it. ;)
React introduced the concept of Hooks in 16.8 which allows us to hook into the lifecycle methods of a functional component. Not everyone is a fan of classes and FYI even a class boils down into a function in a javascript runtime environment. So using a class component just to take advantage of life cycle methods doesn’t makes sense to me.
Scope for this article will be familiarising with most used hooks by a hands on session to understand how we can replace a class component and its lifecycle methods with react hooks.
The Definition
Hooks are functions that let us hook into the React state and lifecycle features from function components. Meaning that hooks allow us to easily manipulate the state of our functional component without needing to convert them into class components. Obviously hooks don’t work inside classes — they let you use React without classes.
Classes confuse both Humans and Machines
We know that components and top-down data flow help us organise a large UI into small, independent, reusable pieces. However, we often can’t break complex components down any further because the logic is stateful and can’t be extracted to a function or another component.
- Huge components that are hard to refactor and test.
- Duplicated logic between different components and lifecycle methods.
- Complex patterns like render props and higher-order components.
Agenda
We will be developing a basic counter application just to keep it simple and bare minimum and we will try use hooks. Below is the already built counter using class component.
From here on , i will be explaining more through code and less through writing.
We will be covering the following hooks -
- useState — We will see different examples of using this hook including best practices, using multiple useStates etc.
- useReducer — An alternative of useState. We will see how we can use this hook to manipulate state just like redux reducers do.
- useEffect — The ultimate replacement of React class lifecycle methods. We will see how we can replace all those componentDid*#*# methods.
- useContext — A rather late addition to React’s context API.
Simple useState
useState is “used” to manage state in functional component.
Two highlights in this snippet:
- we have a very simple function that uses useState and renders an h1 wrapped within a div. The counter is being displayed within the h1 tag.
- The output will be the value 0 displayed on the screen.
Lets dig a bit deeper..
What happens when we call useState()?
useState takes in an initial state as a parameter and returns an array with two objects — State value and an Updater function.
State value is used within the component wherever we want and it doesn’t have to be an object unlike classes. The Updater function is used to update this state value. Instead of calling setState() method we call this updater function.
Fact: Under the hood, react preserves this state value. So, when the component is rendered again because of state changes, the latest value is always available.
Multiple useStates possible?
Of course! We can create multiple state variables by using useState multiple times as shown below.
const [counter, setCounter] = useState(0);
const [error, setError] = useState('');
The counter variable is initialised to 0 and the error variable is initialised to empty string. It returns a pair of values: the current state and a function that updates it.
This is why we write const [counter, setCounter] = useState(0)
. This is similar to this.state.count
and this.setState
in a class, except you get them in a pair.
So what Do Square Brackets Mean? Anybody?
const [count, setState] = useState(0);
This JavaScript syntax is called “array de-structuring”. It means that we’re making two new variables count
and setState
, where count is set to the first value returned by useState
, and setState
of the second one is an updater function which can be used to modify the state (count in this case).
Using updater function
To update the counter we can use the below code, anywhere, maybe on click of a button, etc. setCounter(counter+1);
We will add two buttons, to the page. One will increase the counter when clicked and the other will decrease the counter.
We will write an increment() and decrement() helper function which will be respectively invoked from the button click.
Quick Question: Can we use
setCounter (counter ++)
instead ofsetCounter (counter + 1)
in the above code? Answer in comments.
Using functional parameter in updater function
setState actions are asynchronous and are batched for performance gains. setState()
does not immediately mutate this.state
but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value. There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.
setState alters the state and causes re-rendering. This can be an expensive operation and making it synchronous might leave the browser unresponsive.
If you need to ensure ordering of events after a setState call is made, you can pass a callback function.this.setState({ something: true }, () => console.log(this.state))
Putting error state for negative numbers
Let’s add a feature to show error message, when the counter goes negative.
Let’s create a variable to hold the error state.
const [error, setError] = useState(“”);```
Let’s modify the code to accommodate error state:
{ error.length > 0 && <div className="animated error"> {error} </div> }
Here we are checking for the presence of error and if true we display the message. Lastly, to make it work let’s modify the increment() and decrement() function like so.
useReducer Hook
React now provides a built in useReducer hook, which can be used instead of the redux library. It accepts a reducer of type (state, action) => newState
, and returns the current state paired with a dispatch
method.
Though useState and useReducer both achieve same things but there is a key difference — useState()
returns an updater function and useReducer
returns a dispatch function.
So when to use what?
Usually in big(complex) applications the state that the containers or components maintain is not that simple. Containers maintain a huge state and they have to deal with with changing states based on previous states. Currently we do it within componentsWillReceiveProps()
and dispatch actions based on props and nextProps.
A typical use case will be when different analytics events needs to be fired based on different steps of a progressive form that has steps. It can really get complex like this.
useReducer
is usually preferable in such scenarios when you have complex state logic that involves multiple sub-values.
Let’s define our reducer
function reducer(state, action) {
switch (action.type) {
case "increment":
return {
...state,
counter: state.counter + 1
}
case "decrement":
return {
...state,
counter: state.counter - 1
}
case "reset":
return {
...state,
counter: Number(action.value)
}
default:
return state;
}
}
Assume our “complex” state to be this
const defaultState = {counter: 0, someothervalues: "test"};
Now, within our Counter component, create a reducer and initialise it.
const [state, dispatch] = useReducer(reducer, defaultState);
The increment decrement and reset JSX will change too like this:
UseEffect
The useEffect hook allows us to hook into the lifecycle of the component, like componentDidMount
, componentDidUpdate
, componentWillUnmount
. So this one function plays the role of about three lifecycle events from class component.
The key ingredient of useEffect is the Dependency Array. This Dependency array decides how the useEffect behaves in response to the changes in the component props.
useEffect(() => {
console.log(`RENDER: Count: ${count}`);
},<dependency array>);
We can provide as many props in the array on which we want the useEffect()
to make the component re-render. If any of the defined dependent componentsWillReceiveProps change, the component will re-render.
Now this can have different possible possible forms —
useEffect( () => {})
: Default state i.e. watch for every thing and re-render on every update (componentDidUpdate).useEffect( () => {}, [])
: No dependent props to watch for any change (componentDidMount).useEffect( () => () => {console.log("OverReact!!")}, [])
: Returning a function fromuseEffect()
(componentDidUnMount).
The Affect of UseEffect
By default the useEffect()
has no effect on the component i.e. it runs on every render. But playing with arguments of useEffect we could achieve what different class lifecycle methods can do in a React class without using one.
componentDidUpdate: Simply using useEffect with no dependents array allows us to achieve these lifecycle methods.
componentDidMount: Typically this is required when you need to fetch some data or other operations when the component first mounts.
componentDidUnmount: The useEffect function can return an optional cleanup function. The useEffect here returns a callback function. Here you can do the necessary cleanup like removing event handlers, reference cleanup etc.
Fact: The hooks are asynchronous unlike their class counterparts. This means that they dont block the browser rendering and can happen in the background while browser is busy with its event loop. There are some hooks like
useLayoutEffect
which are synchronous which is obvious as the layout rendering is a blocking change the browser does when repainting happens.
useContext API
Imagine that you have a deeply nested hierarchy of React components. Now, let’s say that the parent needs to pass data, not to its immediate child but to a grandchild or a great-grandchild. This is (in)famously called prop-drilling.
You would have to update the prop at each child component. Redundant, time-consuming, and error-prone!
It is in scenarios like these that useContext
comes in handy.
useContext
creates a “context” at the top level and allows it to be “used” anywhere in the hierarchy.
This context or object/value is available to all components in the hierarchy following that top-level component. This happens without having to pass any props down the hierarchy explicitly.
Up and running with useContext
There are three major things to remember to get using the context API
- Import useContext hook from react lib wherever you want to create a context:
import {useContext} from React;
- React.createContext to create a context that will be sharable amongst the components:
const ThemeContext = React.createContext();
- Provider to provide this context down the line (child components): We will create a component
<ThemeProvider>
that will create a context and pass it down to its children.
Few things to note about the bold line:
First we return JSX from this component that is wrapped in the provider. A provider is something that provides a data/value to this component and its underlying hierarchy.
If you have worked with redux stores, we wrap the entire <App> component in a provider and Provide the redux store to it so this store is now available to all the hierarchy through the reducers involved.
Similarly, here we create a context and wrap our root component in this provider and supply the value/data/object to be accessed by its children directly.
The all new <Counter>
Component:
The above piece of code grabs the theme from the context.And we use this theme as part of the jsx
The <App>
Component
Here is the full code for useContext: