React Hooks [Educative course]

LiveRunGrow
9 min readMay 24, 2023

--

Photo by Maël BALLAND on Unsplash

React Hooks allow you to create stateful logic in functional React components.

Managing states

The useState hook returns the current value of the state and a function to update the state.

Managing side effects

The useEffect hooks allow you to achieve this by creating an inline callback function.

Rules for Hooks

  1. Only call hooks at the top level of a component. Hooks should not be called in if statements, nested or lops.
  2. Only call hooks from react functions. Hooks should not be called from JS functions. Cannot call hooks from class component.

React hooks api

useState

const [state, setState] = useState(INITIAL_STATE);

useEffect

With the useEffect hook, it is possible to replicate behavior for componentDidMount, componentDidUpdate and componentWillUnmount methods.

Some common scenarios to use the useEffect hook with are mentioned below:

  • Add an event listener for a button
  • Fetch data from API when component mounts
  • Perform an action when state or props change
  • Clean up event listeners when the component unmounts

useEffect hook takes the two arguments mentioned below:

  1. It accepts the first argument as a callback function. By default, this function will run after every render but this depends upon the value of the second argument.
  2. The second argument is an optional dependencies array. useEffect hook checks this array to compare the previous and current value for those dependencies and then it runs the callback function but only if one of the dependencies has changed.
useEffect(() => {
// some code
}, [someProp, someState]);

Empty [] means the function passed to useEffect is run only once.

useContext

useContext provides the current context value of the context object passed to it. The useContext hook relies on nearest Context Provider to determine the current value for that context.

import React, { createContext, useContext } from 'react';
const ThemeContext = createContext('green');

const App = () => {
return (
<ThemeContext.Provider value={'green'}>
<Content />
</ThemeContext.Provider>
);
};

const Content = () => {
const theme = useContext(ThemeContext);
return <Button theme={theme} />;
};

If the value of Provider changes, useContext hook will trigger a re-render in the calling component.

React hook with reducer pattern

useReducer hook is an alternative to useState. It offers more control on the next state value based on a given action.

useReducer can be used in the following scenarios:

  • The shape of state object is complex. Maintain multiple items within the same state object.
  • Access previous state value to update the next state.
  • Apply special logic on certain actions to calculate the next state.

The useReducer hook accepts three arguments:

  • Reducer: This is the reducer function. When an action is dispatched, it is called with the current state and the action as first and second argument respectively.
  • Initial State: Supply the value of the initial state here.
  • Init Callback: This parameter is optional. You should only supply it when looking to initialize a state which is based on a custom value or prop at a later stage. When provided, React will call this function as initCb(initialState) to obtain the initial state for the useReducer hook.
const [state, dispatch] = useReducer(reducer, initialState, initCb);

The dispatch function is similar to the setState function seen in useState hook. However, it is a special use case function. When dispatch is called, React will automatically call the reducer with the action provided to the dispatch function.

By using

dispatch({ type: 'REFRESH', message: 'hello' })

It will result in this:

reducer(state, { type: 'REFRESH', message: 'hello' })
import React, { useReducer } from 'react';

const INITIAL_STATE = { count: 0 };

const reducer = (state, action) => {
const { type } = action || {};
if (!type) throw new Error('Action type must be defined');

switch (type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Did you misspell an action type?');
}
};

const App = () => {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE); // !!//

return (
<div className="App">
<h1>Counter: {state.count}</h1>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button
onClick={() => dispatch({ type: 'decrement' })}
>
Decrement
</button>
</div>
);
};

export default App;

The reducer function is defined outside of the App component. Line #7 checks for invalid actions. This is required in case an action is accidentally dispatched without providing an action type. This should be caught and debugged during development as it may lead to a bug later on.

The switch statement on line #9 simply checks for the action type and returns the new value of the count state. It is your responsibility to handle the default case as you see fit. It can return the current state or throw an error. For this lesson, we have opted to throw an error because an unknown action type is not anticipated.

Notice the action is dispatched on button click. Actions are simple JavaScript objects with a type and optional data. In this instance, the type is required as either increment or decrement.

Memoisation hooks

  • Use the useCallback hook when wanting to memoize the callback function. This callback may perform several actions when invoked.
  • Use useMemo hook when wanting to memoize output returned from an expensive compute function. This compute function may perform heavy calculations on each function call.

useCallback hook accepts two arguments: An inline callback function and an array of dependencies. useCallback returns a memoized reference to the callback function when the dependencies do not change.

const memoizedCallback = useCallback(
() => {
calculateFn(input);
},
[input],
);

Similarly, useMemo hook accepts two arguments: A callback that returns the result of an expensive compute function and an array of dependencies. useMemo returns the last cached result if the dependencies do not change.

const memoizedValue = useMemo(() => getExpensiveCalculationResult(input), [input]);

useRef Hook

You can use useRef hook to get the ref of a DOM node. Later, you can use this ref for certain actions related to that node. For example, scrolling to an element position.

useRef provides the ref object with the current property set to the reference of node in DOM.

The following example demonstrates useRef hook usage. On line #4, this hook is called to get the initial ref object. Later, set the ref property on a div to this object on line #11.

Go to top button triggers handleClick event handler. handleClick function simply scrolls the page to the position of the div with text Top section. You can see here that the topSection.current property is used to get access to the DOM node.

import React, { Fragment, useRef } from 'react';

const App = () => {
const topSection = useRef(null);
const handleClick = () => {
window.scrollTo(0, topSection.current.offsetTop);
};

return (
<Fragment>
<div ref={topSection}>Top section</div>
<div className="content">Content section</div>
<div className="content">Content section</div>
<div className="content">Content section</div>
<button onClick={handleClick}>Go to top</button>
</Fragment>
);
};

export default App;

useLayoutEffect hook

useLayoutEffect is an alternative to useEffect for layout updates.

A classic usage for this hook is to truncate long content if the length of the text is too long. useLayoutEffect synchronously re-renders UI before the browser can paint.

The following example demonstrates a basic application that will truncate long text when the height of the parent DOM node is above a certain threshold value.

An inline callback to useLayoutEffect is provided on line #28. Check the height of the wrapper using ref and set the truncate state to true if needed. Because useLayoutEffect can apply updates before the browser paint happens, you will not see any flicker or shrinking of long text which is visible on render. However, it means that it will take longer for the first view to appear in the browser because behind the scenes the paint has been blocked until the useLayoutEffect callback has finished its work.

import React, {
useRef,
useLayoutEffect,
useState,
Fragment
} from 'react';

const MAX_HEIGHT = 120;

const Content = ({ children, truncate }) => {
if (truncate) {
return (
<Fragment>
{React.Children.map(children, (Item, ii) => {
const className = ii > 5 ? 'hidden' : '';
return React.cloneElement(Item, { className });
})}
<button>Read more coming soon</button>
</Fragment>
);
}
return <Fragment>{children}</Fragment>;
};

const App = () => {
const wrapper = useRef(null);
const [truncate, setTruncate] = useState(false);
useLayoutEffect(() => {
if (wrapper.current.clientHeight > MAX_HEIGHT) {
setTruncate(true);
}
}, [wrapper]);

return (
<div className="wrap" ref={wrapper}>
<Content truncate={truncate}>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
<div>Some text</div>
</Content>
</div>
);
};

export default App;

More on useEffect

import React, { useEffect, useState } from 'react';

const INITIAL_STATE = '';

const App = () => {
const [message, setMessage] = useState(INITIAL_STATE);

useEffect(() => {
loadMessage();
});

const loadMessage = () => {
console.log('>> Loading message <<');
try {
fetch('https://json.versant.digital/.netlify/functions/fake-api/message')
.then(res => res.json())
.then(message => {
setMessage(message);
});
} catch (e) {}
};

console.log(`>> Current message is: ${message || 'EMPTY'} <<`);

return <h1>{message}</h1>;
};

export default App;

useEffect runs after every re-render.

  • useEffect hook runs first after the initial render. At this time, it starts the fetch request.
  • useEffect hook runs again after message state is changed with the data received from API then React performs a re-render. This starts a second fetch request.

In the code above, there is no infinite loop. Why?

  • In reality, useEffect hook synchronizes rather than being run after every re-render. It looks at the props and state available in render scope and may skip when nothing changes in the scope. In this case, it skips running the effect after the second fetch is completed because both message and setMessage are the same between renders. This will become clearer when starting to use the dependencies array which explicitly tells useEffect when to skip running the callback function.

Change the useEffect hook like the code snippet below. Add the second argument as an empty array.

useEffect(() => {
loadMessage();
}, []);

When adding an empty array as the dependency array argument, the effect will run once and only after the first render. Because an empty array never changes, useEffect will skip running it on subsequent renders. In short, a componentDidMount method is created by using the useEffect hook! 😎

Manage global state with hooks

The following diagram depicts how a view component can use and update the global state:

  • Generate a global state object and dispatch function using useReducer hook.
  • Store is a React Context object that provides access to the global state and the dispatch function provided by the useReducer hook.
  • Action creator is a JavaScript function to return a new action. Actions are simply a JavaScript object with an action type associated with it.
  • Views can get access to the global state using the Store. Views can also update the state in Store by dispatching actions.

Initial State

The initial state can simply be defined as a JavaScript object. An example is shown below. A global state with the profile object, a greeting string, and a content array is provided.

const initialState = {
profile: null,
greeting: '',
content: []
};

Reducer Function

A skeleton is shown below for a reducer that supports two action types: REFRESH_GREETING and REFRESH_PROFILE. Use this reducer with the useReducer hook.

const reducer = (state, action) => {
switch (action.type) {
case 'REFRESH_GREETING':
// return updated state
case 'REFRESH_PROFILE':
return {
// return updated state
};
default:
return state;
}
};

Global State object and Dispatch function

Using global state object and the dispatch function is as simple as calling useReducer with reducer and initialState.

const [globalState, dispatch] = useReducer(reducer, initialState);

A Store Context with Provider

Finally, you will need a Store. Define it as a context object so it can be accessed from any component.

An implementation that combines previous blocks using StoreContext is shown below. Create StoreContext by using the React.createContext. As seen earlier, you can use the useContext hook to access the value of the StoreContext object. In this case, the value will be set to an array containing globalState and dispatch.

export const StoreContext = React.createContext([]);

const App = () => {
const [globalState, dispatch] = useReducer(reducer, initialState);
return (
<StoreContext.Provider value={[globalState, dispatch]}>
{/* JSX to follow */}
</StoreContext.Provider>
);
};

Others

npx create-react-app my-app
cd my-app
npm start

--

--

LiveRunGrow

𓆉︎ 𝙳𝚛𝚎𝚊𝚖𝚎𝚛 🪴𝙲𝚛𝚎𝚊𝚝𝚘𝚛 👩‍💻𝚂𝚘𝚏𝚝𝚠𝚊𝚛𝚎 𝚎𝚗𝚐𝚒𝚗𝚎𝚎𝚛 ☻ I write & reflect weekly about software engineering, my life and books. Ŧ๏ɭɭ๏ฬ ๓є!