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 ( A3 A4 Hello import Nav from "../Nav"; import Assignment3 from "./a3"; import Assignment4 from "./a4"; import {Routes, Route, Navigate} from "react-router"; function Labs() { return (element={Kanbas ); } 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 Click Hello lifeIs("Good!")}> Click Good onClick={() => { hello(); lifeIs("Great!"); }} > Click Hello 3 ); } 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 add(2, 3)} // onClick={add(2, 3)} className="btn btn-primary"> Pass 2 and 3 to add() ); } 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 Invoke the Function ); } 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 ( ); } 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 onClick={(e) => handleClick(e)} className="btn btn-primary"> Display Event Object {JSON.stringify(event, null, 2)} ); } export default EventObject; // import useState // (more on this later) // 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} onClick={() => { count++; console.log(count); }}> Up onClick={() => { count--; console.log(count); }}> Down ); } 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} setCount(count + 1)}>Up setCount(count - 1)}>Down ); } 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"}
onChange={() => setDone(!done)} /> 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}
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 Add Element {array.map((item, index) => ( {item} deleteElement(index)}> Delete ))} ); } 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} setCounter(counter + 1)}> Increment setCounter(counter - 1)}> Decrement ); } 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} dispatch(increment())}> Increment dispatch(decrement())}> Decrement ); } 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 "../a4/ReduxExamples/AddRedux/addReducer"; export interface LabState { helloReducer: { message: string; }; counterReducer: { count: number; }; addReducer: { sum: number; }; } const store = configureStore({ reducer: { helloReducer, // counterReducer, addReducer, }, }); export default store; To tryout the new reducer, import the add