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.
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.
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.
- 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.
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).
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()
.
Let's rewrite the last example so that cancellation only occurs when needed.
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.
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.
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.