Java Python Managing State in React.js Application
1 Introduction
In this assignment we are going to practice working with application and component level state. State is the collection of
data values stored in the various constants, variables and data structures in an application. Application state is data that
is relevant across the entire application or a significant subset of related components. Component state is data that is
only relevant to a specific component or a small set of related components. If information is relevant across several or
most components, then it should live in the application state. If information is relevant only in one component, or a small
set of related components, then it should live in the component state. For instance, the information about the currently
logged in user could be stored in a profile, e.g., username, first name, last name, role, logged in, etc., and it might be
relevant across the entire application. On the other hand, filling out shipping information might only be relevant while
checking out, but not relevant anywhere else, so shipping information might best be stored in the ShippingScreen or
Checkout components in the component's state. We will be using the Redux state management library to handle
application state, and use React.js state and effect hooks to manage component state.
2 Labs
This section presents React.js examples to program the browser, interact with the user, and generate dynamic HTML. Use
the same project you worked on last assignment. After you work through the examples you will apply the skills while
creating a Kanbas on your own. Using IntelliJ, VS Code, or your favorite IDE, open the project you created in previous
assignments. Include all the work in the Labs section as part of your final deliverable. Do all your work in a new branch
called a4 and deploy it to Netlify to a branch deployment of the same name. TAs will grade the final result of having
completed the whole Labs section.
2.1 Create an Assignment4 Component
To get started, create an Assignment4 component that will host all the exercises in this
assignment. Then import the component into the Labs component created in an earlier
assignment. If not done already, add routes in Labs so that each assignment will appear
in its own screen when you navigate to Labs and then to /a4. Make the Assignment3
component the default element that renders when navigating to http:/ localhost:3000/#/Labs path and map Assignment4
to the /a4 path. You might need to change the lab component route in App.tsx so that all routes after /Labs/* are handled
by the routes declared in the Labs component, e.g., }/>. You might also
want to make Assignment3 the default component by changing the to attribute in the Navigate component in App.tsx, e.g.,
}/>. Use the code snippets below as a guide.
src/Labs/a4/index.tsx src/Nav.tsx src/Labs/index.tsx
import React from "react";
const Assignment4 = () => {
return(
<>
Assignment 4
);
};
export default Assignment4;
import { Link } from "react-router-dom";
function Nav() {
return (
classname="nav-link" to="/Labs/a3" />
A3
classname="nav-link" to="/Labs/a4" />
A4
classname="nav-link" to="/hello" />
Hello
classname="nav-link" to="/Kanbas" />
import Nav from "../Nav";
import Assignment3 from "./a3";
import Assignment4 from "./a4";
import {Routes, Route, Navigate}
from "react-router";
function Labs() {
return (
);
}
export default Nav;
to="a3"/>}/>
element={}/>
element={}/>
);
}
export default Labs;
2.2 Handling User Events
2.2.1 Handling Click Events
HTML elements can handle mouse clicks using the onClick to declare a function to handle the event. The example below
calls function hello when you click the Click Hello button. Add the component to Assignment4 and confirm it behaves as
expected.
src/Labs/a4/ClickEvent.tsx
function ClickEvent() {
const hello = () => {
alert("Hello World!");
};
const lifeIs = (good: string) => {
alert(`Life is ${good}`);
};
return (
Click Event
);
}
export default ClickEvent;
// declare a function to handle the event
// configure the function call
// wrap in function if you need to pass parameters
// wrap in {} if you need more than one line of code
// calling hello()
// calling lifeIs()
2.2.2 Passing Data when Handling Events
When handing an event, sometimes we need to pass parameters to the function handling the event. Make sure to wrap the
function call in a closure as shown below. The example below calls add(2, 3) when the button is clicked, passing
arguments a and b as 2 and 3. If you do not wrap the function call inside a closure, you risk creating an infinite loop. Add
the component to Assignment4 and confirm it works as expected.
src/Labs/a4/PassingDataOnEvent.tsx
const add = (a: number, b: number) => {
alert(`${a} + ${b} = ${a + b}`);
};
function PassingDataOnEvent() {
return (
// function expects a and b
Passing Data on Event
);
}
export default PassingDataOnEvent;
// use this syntax
// and not this syntax. Otherwise you
// risk creating an infinite loop
2.2.3 Passing Functions as Attributes
In JavaScript, functions can be treated as any other constant or variable, including
passing them as parameters to other functions. The example below passes function
sayHello to component PassingFunctions. When the button is clicked, sayHello is
invoked.
src/Labs/a4/PassingFunctions.tsx
function PassingFunctions({ theFunction }: { theFunction: () => void }) {
return (
Passing Functions
);
}
export default PassingFunctions;
// function passed in as a parameter
// invoking function
Include the component in Assignment4, declare a sayHello callback function, pass it to the PassingFunctions component,
and confirm it works as expected.
src/Labs/a4/index.tsx
import PassingFunctions from "./PassingFunctions";
function Assignment4() {
function sayHello() {
alert("Hello");
}
return (
Assignment 4
...
);
}
export default Assignment4;
// import the component
// implement callback function
// pass callback function as a parameter
2.2.4 The Event Object
When an event occurs, JavaScript collects several pieces of information about when the event occurred, formats it in an
event object and passes the object to the event handler function. The event object contains information such as a
timestamp of when the event occurred, where the mouse was on the screen, and the DOM element responsible for
generating the event. The example below declares event handler function handleClick that accepts an event object e
parameter, removes the view property and replaces the target property to avoid circular references, and then stores the
event object in variable event. The component then renders the JSON representation of the event on the screen. Include
the component in Assignment4, click the button and confirm the event object is rendered on the screen.
src/Labs/a4/EventObject.tsx
import React, { useState } from "react";
function EventObject() {
const [event, setEvent] = useState(null);
const handleClick = (e: any) => {
e.target = e.target.outerHTML;
delete e.view;
setEvent(e);
};
return (
Event Object
{JSON.stringify(event, null, 2)}
);
}
export default EventObject;
// import useState
// initialize event
// on click receive event
// replace target with HTML
// to avoid circular reference
// set event object
// so it can be displayed
// button that triggers event
// when clicked passes event
// to handler to update
// variable
// convert event object into
// string to display
2.3 Managing Component State
Web applications implemented with React.js can be considered as a set of functions that transform a set of data
structures into an equivalent user interface. The collection of data structures and values are often referred to as an
application state. So far we have explored React.js applications that transform a static data set, or state, into a static user
interface. We will now consider how the state can change over time as users interact with the user interface and how
these state changes can be represented in a user interface.
Users interact with an application by clicking, dragging, and typing with their mouse and keyboard, filling out forms,
clicking buttons, and scrolling through data. As users interact with an application they create a stream of events that can
be handled by a set of event handling functions, often referred to as controllers. Controllers handle user events and
convert them into changes in the application’s state. Applications render application state changes into corresponding
changes in the user interface to give users feedback of their interactions. In Web applications, user interface changes
consist of changes to the DOM.
2.3.1 Use State Hook
Updating the DOM with JavaScript is slow and can degrade the performance of Web applications. React.js optimizes the
process by creating a virtual DOM, a more compact and efficient version of the real DOM. When React.js renders
something on the screen, it first updates the virtual DOM, and then converts these changes into updates to the actual
DOM. To avoid unnecessary and slow updates to the DOM, React.js only updates the real DOM if there have been changes
to the virtual DOM. We can participate in this process of state change and DOM updates by using the useState hook. The
useState hook is used to declare state variables that we want to affect the DOM rendering. The syntax of the useState
hook is shown below.
const [stateVariable, setStateVariable] = useState(initialStateValue);
The useState hook takes as argument the initial value of a state variable and returns an array whose first item consists of
the initialized state variable, and the second item is a mutator function that allows updating the state variable. The array
destructor syntax is commonly used to bind these items to local constants as shown above. The mutator function not
only changes the value of the state variable, but it also notifies React.js that it should check if the state has caused
changes to the virtual DOM and therefore make changes to the actual DOM. The following exercises introduce various use
cases of the useState.
2.3.2 Integer State Variables
To illustrate the point of the virtual DOM and how changes in state affect changes in the actual DOM, let's implement the
simple Counter component as shown below. A count variable is initialized and then rendered successfully on the screen.
Buttons Up and Down successfully update the count variable as evidenced in the console, but the changes fail to update
the DOM as desired. This happens because as far as React.js is concerned, there has been no changes to the virtual DOM,
and therefore no need to update the actual DOM.
src/Labs/a4/Counter.tsx
import React, { useState } from "react";
function Counter() {
let count = 7;
console.log(count);
return (
Counter: {count}
);
}
export default Counter;
// declare and initialize
// a variable. print changes
// of the variable to the console
// render variable
// variable updates on console
// but fails to update the DOM as desired
For the DOM to be updated as expected, we need to tell React.js that changes to a particular variable is indeed relevant to
changes in the DOM. To do this, use the useState hook to declare the state variable, and update it using the mutator
function as shown below. Now changes to the state variable are represented as changes in the DOM. Implement the
Counter component, import it in Assignment4 and confirm it works as expected. Do the same with the rest of the
exercises that follow.
src/Labs/a4/Counter.tsx
import React, { useState } from "react";
function Counter() {
let count = 7;
const [count, setCount] = useState(7);
console.log(count);
return (
Counter: {count}
);
}
export default Counter;
// import useState
// create and initialize
// state variable
// render state variable
// handle events and update
// state variable with mutator
// now updates to the state
// state variable do update the
// DOM as desired
2.3.3 Boolean State Variables
The useState hook works with all JavaScript data types and structures including booleans,
integers, strings, numbers, arrays, and objects. The exercise below illustrates using the
useState hook with boolean state variables. The variable is used to hide or show a DIV as
well as render a checkbox as checked or not. Also note the use of onChange in the
checkbox to set the value of state variable.
src/Labs/a4/BooleanStateVariables.tsx
import React, { useState } from "react";
function BooleanStateVariables() {
const [done, setDone] = useState(true);
return (
Boolean State Variables
{done ? "Done" : "Not done"}
{done &&
Yay! you are done
);
}
export default BooleanStateVariables;
// import useState
// declare and initialize
// boolean state variable
// render content based on
// boolean state variable value
// change state variable value
// when handling events like
// clicking a checkbox
// render content based on
// boolean state variable value
2.3.4 String State Variables
The StringStateVariables exercise below illustrates using useState with string
state variables. The input field's value is initialized to the firstName state
variable. The onChange attribute invokes the setFirstName mutator function to
update the state variable. The e.target.value contains the value of the input field
and is used to update the current value of the state variable.
src/Labs/a4/StringStateVariables.tsx
import React, { useState } from "react";
function StringStateVariables() {
const [firstName, setFirstName] = useState("John");
return (
String State Variables
{firstName}
dai 写program、Java className="form-control"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}/>
);
}
export default StringStateVariables;
// import useState
// declare and
// initialize
// state variable
// render string
// state variable
// initialize a
// text input field with the state variable
// update the state variable at each key stroke
2.3.5 Date State Variables
The DateStateVariable component illustrates how to work with date
state variables. The stateDate state variable is initialized to the current
date using new Date() which has the string representation as shown
here on the right. The dateObjectToHtmlDateString function can
convert a Date object into the YYYY-MM-DD format expected by the
HTML date input field. The function is used to initialize and set the
date field's value attribute so it matches the expected format.
Changes in date field are handled by the onChange attribute which
updates the new date using the setStartDate mutator function.
src/Labs/a4/DateStateVariable.tsx
import React, { useState } from "react";
function DateStateVariable() {
const [startDate, setStartDate] = useState(new Date());
const dateObjectToHtmlDateString = (date: Date) => {
return `${date.getFullYear()}-${date.getMonth() + 1 < 10 ? 0 : ""}${
date.getMonth() + 1
}-${date.getDate() + 1 < 10 ? 0 : ""}${date.getDate() + 1}`;
};
return (
Date State Variables
{JSON.stringify(startDate)}
{dateObjectToHtmlDateString(startDate)}
className="form-control"
type="date"
value={dateObjectToHtmlDateString(startDate)}
onChange={(e) => setStartDate(new Date(e.target.value))}
/>
);
}
export default DateStateVariable;
// import useState
// declare and initialize with today's date
// utility function to convert date object
// to YYYY-MM-DD format for HTML date
// picker
// display raw date object
// display in YYYY-MM-DD format for input
// of type date
// set HTML input type date
// update when you change the date with
// the date picker
2.3.6 Object State Variables
The ObjectStateVariable component below demonstrates how to work with object state
variables. We declare person object state variable with initial property values name and age.
The object is rendered on the screen using JSON.stringify to see the changes in real time.
Two value of two input fields are initialized to the object's person.name string property and
the object's person.age number property. As the user types in the input fields, the onChange
attribute passes the events to update the object's property using the setPerson mutator
functions. The object is updated by creating new objects copied from the previous object
value using the spreader operator (...person), and then overriding the name or age property
with the target.value.
src/Labs/a4/ObjectStateVariable.tsx
import React, { useState } from "react";
function ObjectStateVariable() {
const [person, setPerson] = useState({ name: "Peter", age: 24 });
return (
Object State Variables
{JSON.stringify(person, null, 2)}
value={person.name}
onChange={(e) => setPerson({ ...person, name: e.target.value })}
/>
value={person.age}
onChange={(e) => setPerson({ ...person,
age: parseInt(e.target.value) })}
/>
);
}
export default ObjectStateVariable;
// import useState
// declare and initialize object state
// variable with multiple fields
// display raw JSON
// initialize input field with an object's
// field value
// update field as user types. copy old
// object, override specific field with new
// value
// update field as user types. copy old
// object,
// override specific field with new value
2.3.7 Array State Variables
The ArrayStateVariable component below demonstrates how to work with array state variables. An array of integers if
declared as a state variable and function addElement and deleteElement are used to add and remove elements to and
from the array. We render the array as a map of line items in an unordered list. We render the array's value and a Delete
button for each element. Clicking the Delete button calls the deleteElement function which passes the index of the
element we want to remove. The deleteElement function computes a new array filtering out the element by its position
and updating the array state variable to contain a new array without the element we filtered out. Clicking the Add Element
button invokes the addElement function which computes a new array with a copy of the previous array spread at the
beginning of the new array, and adding a new random element at the end of the array.
src/Labs/a4/ArrayStateVariable.tsx
import React, { useState } from "react";
function ArrayStateVariable() {
const [array, setArray] = useState([1, 2, 3, 4, 5]);
const addElement = () => {
setArray([...array, Math.floor(Math.random() * 100)]);
};
const deleteElement = (index: number) => {
setArray(array.filter((item, i) => i !== index));
};
return (
Array State Variable
- {array.map((item, index) => (
{item}
- ))}
);
}
export default ArrayStateVariable;
// import useState
// declare array state
// event handler appends
// random number at end of
// array
// event handler removes
// element by index
// button calls addElement
// to append to array
// iterate over array items
// render item's value
// button to delete element
// by its index
2.3.8 Sharing State Between Components
State can be shared between components by passing references to state variables and/or functions that update them.
The example below demonstrates a ParentStateComponent sharing counter state variable and setCounter mutator
function with ChildStateComponent by passing it references to counter and setCounter as attributes.
src/Labs/a4/ParentStateComponent.tsx
import React, { useState } from "react";
import ChildStateComponent from "./ChildStateComponent";
function ParentStateComponent() {
const [counter, setCounter] = useState(123);
return (
Counter {counter}
counter={counter}
setCounter={setCounter} />
);
}
export default ParentStateComponent;
//
The ChildStateComponent can use references to counter and setCounter to render the state variable and manipulate it
through the mutator function. Import ParentStateComponent into Assignment4 and confirm it works as expected.
src/Labs/a4/ChildStateComponent.tsx
function ChildStateComponent({ counter, setCounter }:
{ counter: number;
setCounter: (counter: number) => void;}) {
return (
Counter {counter}
);
}
export default ChildStateComponent;
//
2.4 Managing Application State
The useState hook is used to maintain the state within a component. State can be shared across components by passing
references to state variables and mutators to other components. Although this approach is sufficient as a general
approach to share state among multiple components, it is fraught with challenges when building larger, more complex
applications. The downside of using useState across multiple components is that it creates an explicit dependency
between these components, making it hard to refactor components adapting to changing requirements. The solution is to
eliminate the dependency using libraries such as Redux. This section explores the Redux library to manage state that is
meant to be used across a large set of components, and even an entire application. We'll keep using useState to manage
state within individual components, but use Redux to manage Application level state.
To learn about redux, let's create a redux examples component that will contain several simple redux examples. Create an
index.tsx file under src/Labs/a4/ReduxExamples/index.tsx as shown below. Import the new redux examples component
into the assignment 4 component so we can see how it renders as we add new examples. Reload the browser and
confirm the new component renders as expected.
src/Labs/a4/ReduxExamples/index.tsx src/Labs/a4/index.tsx
import React from "react";
const ReduxExamples = () => {
return(
Redux Examples
);
};
export default ReduxExamples;
import React from "react";
import ReduxExamples from "./redux-examples";
const Assignment4 = () => {
return(
<>
Assignment 4
...
);
};
export default Assignment4;
2.4.1 Installing Redux
As mentioned earlier we will be using the Redux state management library to handle application state. To install Redux,
type the following at the command line from the root folder of your application.
$ npm install redux --save
After redux has installed, install react-redux and the redux toolkit, the libraries that integrate redux with React.js. At the
command line, type the following commands.
$ npm install react-redux --save
$ npm install @reduxjs/toolkit --save
2.4.2 Create a Hello World Redux component
To learn about Redux, let's start with a simple Hello World example. Instead of maintaining state within any particular
component, Redux declares and manages state in separate reducers which then provide the state to the entire
application. Create helloReducer as shown below maintaining a state that consists of just a message state string
initialized to Hello World.
src/Labs/a4/ReduxExamples/HelloRedux/helloReducer.ts
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
message: "Hello World",
};
const helloSlice = createSlice({
name: "hello",
initialState,
reducers: {},
});
export default helloSlice.reducer;
//
Application state can maintain data from various components or screens across an entire application. Each would have a
separate reducer that can be combined into a single store where reducers come together to create a complex, application
wide state. The store.tsx below demonstrates adding the helloReducer to the store. Later exercises and the Kanbas
section will add additional reducers to the store.
src/Labs/store/index.tsx
import { configureStore } from "@reduxjs/toolkit";
import helloReducer from "../a4/ReduxExamples/HelloRedux/helloReducer";
export interface LabState {
helloReducer: {
message: string;
};
}
const store = configureStore({
reducer: {
helloReducer,
},
});
export default store;
//
The application state can then be shared with the entire Web application by wrapping it with a Provider component that
makes the state data in the store available to all components within the Provider's body.
src/Labs/index.tsx
...
import store from "./store";
import { Provider } from "react-redux";
function Labs() {
return (
Labs
...
);
}
export default Labs;
//
Components within the body of the Provider can then select the state data they want using the useSelector hook as
shown below. Add the HelloRedux component to ReduxExamples and confirm it works as expected.
src/Labs/a4/ReduxExamples/HelloRedux/index.tsx
import { useSelector, useDispatch } from "react-redux";
import { LabState } from "../../../store";
function HelloRedux() {
const { message } = useSelector((state: LabState) => state.helloReducer);
return (
Hello Redux
{message}
);
}
export default HelloRedux;
//
2.4.3 Counter Redux - Dispatching Events to Reducers
To practice with Redux, let's reimplement the Counter component using Redux. First create counterReducer responsible
for maintaining the counter's state. Initialize the state variable count to 0, and reducer function increment and decrement
can update the state variable by manipulating their state parameter that contain state variables as shown below.
src/Labs/a4/ReduxExamples/CounterRedux/counterReducer.tsx
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
count: 0,
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.count = state.count + 1;
},
decrement: (state) => {
state.count = state.count - 1;
},
},
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
//
Add the counterReducer to the store as shown below to make the counter's state available to all components within the
body of the Provider.
src/Labs/store/index.tsx
import { configureStore } from "@reduxjs/toolkit";
import helloReducer from "../a4/ReduxExamples/HelloRedux/helloReducer";
import counterReducer from "../a4/ReduxExamples/CounterRedux/counterReducer";
export interface LabState {
helloReducer: { message: string; };
counterReducer: {
count: number;
};
}
const store = configureStore({
reducer: {
helloReducer,
counterReducer,
},
});
export default store;
//
The CounterRedux component below can then select the count state from the store using the useSelector hook. To invoke
the reducer function increment and decrement use a dispatch function obtained from a useDispatch function as shown
below. Add CounterRedux to ReduxExamples and confirm it works as expected.
src/Labs/a4/ReduxExamples/CounterRedux/index.tsx
import { useSelector, useDispatch } from "react-redux";
import { LabState } from "../../../store";
import { increment, decrement } from "./counterReducer";
function CounterRedux() {
const { count } = useSelector((state: LabState) => state.counterReducer);
const dispatch = useDispatch();
return (
Counter Redux
{count}
);
}
export default CounterRedux;
//
2.4.4 Passing Data to Reducers
Now let's explore how the user interface can pass data to reducer functions. Create a reducer that can keep track of the
arithmetic addition of two parameters. When we call add reducer function below, the parameters are encoded as an object
into a payload property found in the action parameter passed to the reducer function. Functions can extract parameters a
and b as action.payload.a and action.payload.b and then use the parameters to update the sum state variable.
src/Labs/a4/ReduxExamples/AddRedux/addReducer.tsx
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
sum: 0,
};
const addSlice = createSlice({
name: "add",
initialState,
reducers: {
add: (state, action) => {
state.sum = action.payload.a + action.payload.b;
},
},
});
export const { add } = addSlice.actions;
export default addSlice.reducer;
//
Add the new reducer to the store so it's available throughout the application as shown below.
src/Labs/store/index.tsx
import { configureStore } from "@reduxjs/toolkit";
import helloReducer from "../a4/ReduxExamples/HelloRedux/helloReducer";
import counterReducer from "../a4/ReduxExamples/CounterRedux/counterReducer";
import addReducer from ".