lilt: Lightweight Interactive Learning Tool

Topic 7

React Part 3

In this session, the final session of three on React, we will look at:

  • Running external functionality, such as connecting to a web server, with effects
  • Preserving data between renderings using refs
  • Using context to ease the process of passing data through multiple levels of a composable hierarchy

Introduction to Effects

Commonly we might want some non-React code to run each time we render. For example, we might wish to perform some complex and time-consuming calculations. We cannot do this directly inside the component function, as the component function is intended for rendering only; it should execute quickly and not contain time-consuming code.

Instead, we can create an effect. An effect is a function which we might want to run either when the component first loads, each time the component renders, which performs some kind of external operation. We specify effects with useEffect(), passing in a function as a parameter.

Here is an example of an effect which loads from an API which returns special offers:


Note how we make use of useEffect() to run a piece of code which runs once every render. This code fetches all special offers from an external API and then renders the data as JSX.

Note also that useEffect() in this example takes a second parameter, an empty array ([]).

What is this empty array? It's an array of dependencies: a list of props or state variables which will trigger a re-run of the effect if they change. You can fill this array with props or state variables if you want to re-run the effect if those props or state variables change.

If the array is empty, however, you are specifying that the effect should never be re-run: it should only run once, when the component first loads. In this case this is what we need as we need only fetch the list of special offers when the component loads; there is no need to re-fetch the list every time the component is re-rendered.

Effects and async/await

You cannot make an effect an async function. If you wish to do asynchronous tasks (e.g. AJAX) from an effect directly, you should use the then-based syntax for promises instead. See week 3.

You can find out more about effects in the React documentation here.

Refs - preserving data between renderings

Sometimes it might be desirable to preserve data between renderings, without using state. State is intended for data that will be rendered (hence, updating state triggers a re-rendering) but there are some cases where we might wish to preserve data for other reasons. For example, we might want to keep hold of a timer handle used to control a timer function. Or we might want to use objects which are part of other APIs, such as mapping APIs (e.g. Leaflet).

We could use global variables, declared outside the component, for this in theory. However this is not considered good design as components are supposed to be pure functions (ref React docs) which perform a task (rendering a component) without having any side-effects (e.g. changing variables external to the component).

Instead we can use a ref (reference). A ref is a variable that can be changed within the component without causing a re-rendering.

A good example to store in a ref might be a timer variable. You might know that you can use setInterval() in JavaScript/TypeScript to run code every so many milliseconds. setInterval() returns a timer handle which can then be used to control (stop/start) the timer.

So how can we do this in React? We can store the timer as a ref, because we need to preserve it between renderings.

Here is some code to do this.


What's this doing?

  • It's a component simulating a timer, which starts from 0 and increments every second when 'start' is clicked, and stops when 'stop' is clicked.
  • It uses an interval function to do this. The interval function resets the currentTime state variable to the current time (in seconds since January 1st 1970) every second.
  • The interval function returns a timer handle, which can be used to stop the timer later.
  • The handle is stored in a ref as we need to preserve it between renderings, but isn't being rendered itself and we do not want any changes to it to trigger a re-rendering. Note that to access the actual data in the ref, we need to use its current property, e.g. timerHandle.current in this example.
  • The component displays the number of seconds that have passed, by subtracting the initial time from the current time. Date.now() gives the number of milliseconds since January 1st 1970, so we divide by 1000 and round to the nearest integer to convert this to seconds. The arrow function will run every second (1000 milliseconds).
  • When we click the stop button, we clear the timer handle:

Refs and Effects with Maps

Here is an example using both refs and effects. We use a ref to hold a Leaflet map and an effect to initialise the map.

Note how in our effect, we check that the map (a ref) is null. If it's null, it means it hasn't been initialised yet so we initialise it. If not, we do nothing - we don't want to initialse the map twice. Effects, by default, run each time the component is rendered, so we have to ensure that we do not re-initialise the map each time the component is rendered, which would be very wasteful and would also constantly reset the map to its default location.


In this example, the current map latitude and longitude are stored in state and displayed. Also, when the user clicks the button to change the location, the map moves to that location.

The interesting thing, though, is that the map is stored as a ref. It's not a state variable to be rendered, it's an external entity to React which needs to be kept around between renders. Refs are a good way of handling such external objects. So we initialise it as a ref, and then update the position of the map using that ref in setPos().

We also handle map moveend events (when the user stops dragging the map) so that the lat and lon state variables are updated to hold the new centre position of the map.

Finally you will note that the map initialisation code is written inside an arrow function passed to something we have not discussed yet: useEffect(). What is this? The next section will explain!

You can find out more about refs in the React documentation here

Alternative approach

The example above showed one way to prevent effects running on each render, by setting up a ref and checking whether it's null. However, there is a second, automatic, way of doing this. We can pass an empty array as the second parameter to useEffect(). This will cause the effect to run just once, on startup. The example below is the Leaflet map example re-written to do this:



What is this empty array? It's an array of dependencies: a series of props or state variables which will trigger a re-run of the effect if they change. You can fill this array with props or state variables if you want to re-run the effect if any props or state variables change. If the array is empty, however, you are specifying that the effect should never be re-run: it should only run once when the component first loads.

You can find out more about effects in the React documentation here.

Exercise 1

This exercise checks your understanding of the topics covered this week, including effects.
  • Imagine you are developing an online shopping application. When the site loads, the user should see a list of all product categories. You need to fetch the product categories from the web, and display them, in a React component when the component first loads. How would you do this?
  • Imagine there is a separate component to display all products in a particular category. The category is passed into the component as a prop, and the component then fetches the products in that category from the web using an effect. What should the second argument of useEffect() be in this case?
Submission disabled now you have completed or the notes have been made public.

Answer to exercise 1

  1. For the first question we want to load all product categories when the application first loads. An effect is ideal for this as we want the communication with the server to take place in the background without slowing down rendering. However what do we need for the second argument? The key thing is that we only need to load the data when the application first loads, not on every render. So we would provide an empty array [] as the second argument.
  2. For the second question we want to load all products within a particular category. The category is passed in as a prop. We need to load the products in that category whenever the value of the prop changes - so that if a new category is selected, the products in this category appear. Therefore we need to specify the prop as the second argument of useEffect() so that the effect runs whenever the prop changes:

Context

Imagine that we need to pass down state through several layers in the component hierarchy. We can do this by passing state down through props, but it can become cumbersome if we have to pass many items of data down through the tree to the lowest level components.

Example

This is an example of sharing state through props with three levels of component, App, Search and SearchResults.

App

ModeChooser (used to select the mode)

SearchResults

You should be able to see how the darkMode is passed down from App to SearchResults via Search using props. It works, and in this case its acceptable, but could become cumbersome if there were many props to pass through from the top component to the lower components.

Using Context

What we can do instead is to store information that needs to be shared in this way in context. We store the dark mode in context and then access it directly from the components lower down in the hierarchy.

How do we create a context?

First you have to create a context in a separate file:

This creates a context called DarkModeContext and exports it: if there is no value yet, it will take the value false.

We can then use it in our top-level, parent component (App):

Note how we:

  • import the context from the DarkModeContext module;
  • when rendering Search, we then set up a DarkModeContext.Provider surrounding any child elements. This provides the given context to the child elements, note it has a value of darkMode, which is the name from the state.

In the SearchResults component (and in the Search compnent) we can then directly access the context via React's useContext() API:

Note how we reference the context we need using useContext with the desired context (the context exported from DarkModeContext.ts) to access the name. The value used will be that provided by the provider in the parent context.

Passing multiple contexts

We can pass multiple contexts by nesting providers, e.g.

and we can then use them in child components separately, e.g:

Coding Exercise

Ensure you have done Exercise A and B from last week first.

  • Make a copy of your Week 5 work and install Leaflet in addition.
  • Try out the first of the Leaflet map examples (LeafletApp not LeafletApp2).
  • Separate out the map into its own Map component, receiving lat and lon as props. Do not pass an L.LatLng object in as a prop; instead pass in the latitude and longitude separately. It should return JSX containing only the map. Initialise the map in the Map component when it first loads, but ensure that the map is reset to the new location whenever the lat and lon props change. Also, change the main LeafletApp component so that includes your new map component in its rendered JSX, and so that the latitude and longitude from the state variable are passed into the new map component, as props.
  • Add a callback function to your Map component which gets called when the user finishes moving the map. The callback should take the new map position as a parameter, and link to an appropriate function in the LeafletApp component to update the state variable holding the map position.
  • Making use of an effect, write a separate component to do an AJAX search for places by name using this API:

    where place_name is the place to search for. The component should contain a form allowing the user to enter the place name, and store the place name as state. When the AJAX search has completed, store the results in state and render the list of results as JSX. Note that this API provides OpenStreetMap data and makes use of the Nominatim web service.

  • Lift the state variable containing the search results up to your main component, and modify your app so that the search results are shown both as a list and as markers on the map.
  • Try adding dark mode to the application using context, as in the example above.

Advanced exercise

If you get that done, develop the search results component so that each result should have a button labelled "Go to this location". When this button is clicked, the map latitude and longitude should be set to that location.