More complex components will contain sub-components. For example, imagine an extended version of the shopping cart in which there are two React components, one for adding a product, and one for showing the list of products.
Why would you want to do this? You might want to use the component containing the shopping cart in another application, where the products are loaded in from the web rather than input by the user, for example. Or you might want the "add product" component to connect to a database and store the tasks in a database, rather than displaying them immediately.
So by separating out the component into smaller, separate components, it makes each component reusable in different situations, and you can use one component without the other.
What you are probably asking is: how can the contents of the first sub-component (to add a product) be passed to the second (the list of products)? The recommended approach (see the React documentation) is to store information that needs to be shared between the two components as state of the parent. This is known as lifting state up (i.e. up to the parent component).
This topic is discussed in the React documentation
We will illustrate this with a simpler example. This example contains three components:
InputComponent
to allow the user to input a name;GreetingComponent
to show a greeting to the user (using the name input in the InputComponent
);AppComponent
to manage the application as a whole and to store the state. The name entered in the InputComponent
will be "lifted up" to the AppComponent
.import React from 'react';
function AppComponent({title, defaultName}) {
const [name, setName] = React.useState(defaultName);
function updateState(name) {
setName(name);
}
return (
<div>
<InputComponent title={title} passBackUserInput={updateState} />
<GreetingComponent name={name} />
</div>
)
}
export default AppComponent;
import React from 'react';
function InputComponent({title, passBackUserInput}) {
function updateName() {
const n = document.getElementById('name').value;
passBackUserInput(n);
}
return (
<div>
<h1>{title}</h1>
<fieldset>
<input type='text' id='name' onChange={updateName} />
</fieldset>
</div>
)
}
export default InputComponent;
import React from 'react';
function GreetingComponent({name}) {
return (
<div>{name}</div>
)
}
export default GreetingComponent;
What is happening here? The diagram gives a general idea:
Firstly note the parent component, AppComponent
. Note that the parent component stores the name in its state. Note how the name is passed down to the two sub-components, InputComponent
and GreetingComponent
, as their props. In a similar way to the text field value being tightly bound to the state, this will tightly bind the props of the sub-components to the state of the parent.
In the GreetingComponent
we display the name prop, which as we have seen, is tightly bound to the state of the parent because it's passed down from the parent.
The important thing with the InputComponent
is that it needs a way to send the name the user entered back up to the AppComponent
, because that is where the name is being stored in the state. To do this, we pass a method to update the state into the InputComponent
, which can then call it. We do this by passing the updateState()
method of AppComponent
(which updates the state) into the InputComponent
as a prop called passBackUserInput
.
<InputComponent passBackUserInput={updateState} name={name}/>
Thus, this prop, i.e. passBackUserInput()
, can be called as a method from the InputComponent
. This is precisely what happens when we encounter an onChange event; we first call the InputComponent
's own updateName() and then pass the name in the text field to passBackUserInput(). Since the passBackUserInput() property is set equal to the updateState() method in AppComponent, any change to the name in the text field will update the state of the parent component. Then, since the name
prop of the GreetingComponent is tightly bound to the parent's state, any updates in the text field will update the GreetingComponent
.
React documentation: here
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 to run code every so many milliseconds, e.g.
const startTime = Math.round(Date.now() / 1000);
const timerHandle = setInterval( () => {
const timeNow = Math.round(Date.now() / 1000);
document.getElementById('seconds').innerHTML = `${timeNow - startTime} seconds have passed.`;
}, 1000);
In the example above the arrow function which 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). Note also how setInterval()
returns a handle on the timed function (stored in the variable timer
here) which can be used later to cancel the timer:
clearInterval(timerHandle);
So how can we do this in React? We can store the timer as a ref, because we need to preserve it between renderings. For example:
import React from 'react';
function Timer() {
const timerHandle = React.useRef(null);
const [startTime, setStartTime] = React.useState(0);
const [currentTime, setCurrentTime] = React.useState(0);
function startTimer() {
// If the timer is currently running...
if(timerHandle.current == null) {
// Store the start time in state
const initialTime = Math.round(Date.now() / 1000);
setStartTime(initialTime);
setCurrentTime(initialTime);
// Start the interval function
timerHandle.current = setInterval ( () => {
// Every second, store the current time in state
const timeNow = Math.round(Date.now() / 1000);
setCurrentTime(timeNow);
} , 1000 );
}
}
function stopTimer() {
if(timerHandle.current != null) {
clearInterval(timerHandle.current);
timerHandle.current = null;
}
}
// Render the number of seconds that have passed by subtracting the
// current time from the start time
return( <div>
Time: {currentTime - startTime} seconds <br />
<input type='button' value='start' onClick={startTimer} />
<input type='button' value='stop' onClick={stopTimer} />
</div>
);
}
export default Timer;
What's this doing?
time
state variable to the current time (in seconds since January 1st 1970) every second. The timer handle (timerHandle
) is cleared when the user clicks 'stop'.- In the rendering, the time in seconds is calculated by subtracting the initial time (startTime
, another state variable) from the current time (the time
state variable).current
property, e.g. timerHandle.current
in this example.Here is another example of using a ref, to hold a Leaflet map. Note that React.useEffect()
sets up an effect, which will be discussed below. An effect is a function which can run either on every render or on start-up only - see below for more details.
import React from 'react';
import 'leaflet';
function LeafletMap({lat1, lon1}) {
const map = React.useRef(null);
const [lat, setLat] = React.useState(lat1 || 51.05);
const [lon, setLon] = React.useState(lon1 || -0.72);
React.useEffect( ()=> {
// Initialise the map ref if it hasn't been initialised already
if(map.current === null) {
map.current = L.map("map1");
// Set the map up in the normal way
L.tileLayer
("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
{ attribution: "Copyright OSM contributors, ODBL" } ).addTo(map.current);
const pos = [lat, lon];
map.current.setView(pos, 14);
// Handle the map moveend event by updating the state appropriately
map.current.on("moveend", e=> {
const centre = map.current.getCenter();
setLat(centre.lat);
setLon(centre.lng);
});
}
});
return(
<div>
Lat: <input id='lat' />
Lon: <input id='lon' />
<input type='button' value='go' onClick={setPos} />
<p>Map centred at: {lat} {lon}</p>
<div id="map1" style={{width:"800px", height:"600px"}}></div>
</div>
);
function setPos() {
const lt = document.getElementById('lat').value;
const lng = document.getElementById('lon').value;
setLat(lt);
setLon(lng);
map.current.setView([lt, lng], 14);
}
}
export default LeafletMap;
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: React.useEffect()
. What is this? The next section will explain!
React documentation: here
Commonly we might want some non-React code to run each time we render. For example, we might wish to fetch data from an external web API. 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. We could use an event handler (e.g. a button click handler), to perform these kinds of operations, but in some cases we might want the action to occur without such an event, e.g. when the component first loads or each time we render.
Instead, we can create an effect
. An effect is a function which we might want to run each time the component renders, which performs some kind of external operation. We specify effects with React.useEffect()
, passing in a function as a parameter. An example is given above in the mapping component. Note that the code to initialise the map is placed within an arrow function passed as a parameter to React.useEffect()
.
Note how in our effect, we check that the map (a ref) is null
. If it's null
, it means we haven't initialised it yet so we initialise it. If not, we do nothing. Effects 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.
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 WAD week 3.
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 React.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:
import React from 'react';
import 'leaflet';
function LeafletMap2({lat1, lon1}) {
const map = React.useRef(null);
const [lat, setLat] = React.useState(lat1 || 51.05);
const [lon, setLon] = React.useState(lon1 || -0.72);
React.useEffect( ()=> {
// As effect will run only once, there is no need to check if map.current is null
map.current = L.map("map1");
// Set the map up in the normal way
L.tileLayer
("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
{ attribution: "Copyright OSM contributors, ODBL" } ).addTo(map.current);
const pos = [lat, lon];
map.current.setView(pos, 14);
// Handle the map moveend event by updating the state appropriately
map.current.on("moveend", e=> {
const centre = map.current.getCenter();
setLat(centre.lat);
setLon(centre.lng);
});
},[]);
return(
<div>
Lat: <input id='lat' />
Lon: <input id='lon' />
<input type='button' value='go' onClick={setPos} />
<p>Map centred at: {lat} {lon}</p>
<div id="map1" style={{width:"800px", height:"600px"}}></div>
</div>
);
function setPos() {
const lt = document.getElementById('lat').value;
const lng = document.getElementById('lon').value;
setLat(lt);
setLon(lng);
map.current.setView([lt, lng], 14);
}
}
export default LeafletMap2;
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 such 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.
https://hikar.org/webapp/nomproxy?q=place_name, where
place_name
is the place to search for. When the AJAX search has completed, set the lat
and lon
inside the state to be the latitude and longitude of the first result within the JSON returned by the API. Note that this API provides OpenStreetMap data and makes use of the Nominatim web service.Advanced: If you get that done, develop the map application so you have a SearchResults
component (within the parent component) which should list all results (not just the first). Store the results in state. Also show all results as markers on the map. Each result should have a button labelled "Go to this location" which, when clicked, should set the latitude and longitude of the map to that location.
Here is an example of some JSX (which you would generate from map()
) showing how you could pass parameters into an onClick
handler. You could set the onClick
handler to be an arrow function, and then inside the arrow function, call another function (goTo()
here) which moves the map to the specified location.
<input type='button' onClick={() => { goTo(result.lat,result.lon) } } />