Day 3 - June 29, 2022

Topics Covered

  • Introduction to Hooks
  • useState and useEffect
  • Lifecycle methods for Functional components
  • Sharing data

Introduction to Hooks

In order to build components with state, we learned that we must use classes to define the components. This leads to writing bloated code that isn't as easy to understand. Thanks to the introduction of React Hooks, you can use simple functions to implement your components without losing the ability of maintining state.

The React Hook API introduces the useState() function. useState() enables your functional component to become stateful. Let's look at a code example. In the example we will configure and use initial state values.

Code Example
Output

Welcome to WEB530
Established in 1967

The App component is a stateful functional component thanks to the useState() hook. The example initializes two pieces of state, name and year, by calling useState() once for each state value. You can have as many pieces of state that you need, simply call useState() for each. Although it is possible to store a JavaScript object as a state value, it can complicate things and isn't recommended.

Look closely at the code example. You will notice that when we call useState(), square brackets surround the variable name. Check out line 5 where it says [name]. The reason we use the brackets is because useState() returns an array and so can take advantage of array destructuring syntax. The first value of the array is the state value, this is later injected into the UI. The second value of the array is a function that can be used to update the value. Since we are not changing the state, we discard this value. Now let's check out an example where the state is changed.

Code Example

In the code example, you will notice we now destructure multiple variables from the array returned by useState(). setName refers to a function that can be used to change the State. The code above automatically calls the setName() function any time the text is changed but we could call it manually using something like setName("New Name"). After setName() is called the name is updated and the UI refreshed.

With functional components that use the useState() hook, you can see that we will need to update each piece of state individually. Unlike setState() where we could merge multiple pieces of state at the same time, we now have individual functions.

Lifecycle methods for Functional components

Our React components often need the ability to perform initialization or cleanup. We saw that this is possible with class-based components by introducing the componentDidMount() and componentWillUnmount() methods to the component class. With functional components we cannot perform initialization and cleanup the same way. Instead, we must take advantage of the useEffect() hook.

The useEffect() hook is used to run "side-effects" in your component. Remember, functional components only have one job, that is, to return JSX content. If a component needs to do something else this should be done by a useEffect() hook.

So, what kind of initialization or cleanup would a component need to handle? There are many reasons for developers to implement this functionality. A common example could be, loading data. You could use the fetch API to fetch data from a remote device and display it on your UI. Fetching is an asyncronous operation and if not implemented correctly, may introduce race conditions or buggy behaviour. Using useEffect() we can minimize these problems. During the cleanup routine, it is a good idea to ensure the fetch has completed and if not, cancel it.

Definitions
Race condition
Occurs when the sequence or timing of events is important. A race condition can occur when events execute in an undesirable order. Still unclear?

The useEffect() hook expects a function as an argument. The function is called after the component finishes rendering.

Code Example

After about two seconds, the user is fetched and the UI is updated. If the user is navigating around the app, there is a chance that they may unmount the component before a response is received. If this happens an error will occur because the callback function will attempt to update the state of a component that no longer exists. To prevent this condition, useEffect() has a mechanism to clean up resources (such as the pending fetch request).

Code Example

By adding return () => { ... } to the end of the useEffect() function, React now knows that it should clean-up the component and will call the returned method when the component is unmounted. But what if the promise has already completed, there is no need to attempt cancellation.

When setting up the useEffect() we can pass a second argument, an array of values. These are values that React should watch so that it can determine if clean-up is necessary. For example, if the promise is resolved, don't call cancel().

Note
Promises do not support cancellation so the cleanup logic can get quite tricky. Luckily libraries such as Bluebird implement additional functionality (such as the ability to abort a promise). The code examples on this page use Bluebird for cancellation.

Let's rewrite the last example so that cancellation only occurs when needed.

Code Example

Sharing data

Typically, React components will fetch the data that they and their children need but sometimes it is convenient for our app to load global data that can be shared across multiple components. To share global data, you can use the React Context API. Components that are rendered within a context are able to access the data provided by the context.

Code Example

Study the code example. On line 4, we create a new context by calling createContext(). Next we have our utility function for fetching user data. This is similar to the fetchUser() function in the other code examples except that we are setting the state value to an object rather than setting individual properties. Finally, we have a UserContextProvider() function. The job of this component is to fetch the user information and update the context when the information has been received back by the API.

You will notice the UserContextProvider() function accepts an argument { children }. You will also notice that it renders a <UserContext.Provider> component. The <UserContext.Provider> will act as a container sharing data with the child components. In the example, we are sharing the user object by passing it into the value attribute.

In our main app component, we render multiple <UserInfo> components used for displaying the user name. This component grabs the shared data from the parent context provider by calling useContext(). As the data is changed, the UI is updated.

Output

User name is ...
User name is ...
User name is ...

After about two seconds, the output is changed:

User name is Wayne
User name is Wayne
User name is Wayne

It is worth mentioning that the <UserInfo> component has no knowledge of the global data. Instead of passing the data down from the top-level component as property values, we can rely on the useContext() function to provide access to the global data. Components (like the inner <View> components) that have nothing to do with the data don't need (or care) about it.