联系方式

  • QQ:99515681
  • 邮箱:99515681@qq.com
  • 工作时间:8:00-21:00
  • 微信:codinghelp

您当前位置:首页 >> Java编程Java编程

日期:2024-03-22 08:16

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., <Route path="/Labs/*" element={<Labs/>}/>. You might also

want to make Assignment3 the default component by changing the to attribute in the Navigate component in App.tsx, e.g.,

<Route path="/" element={ <Navigate to="a3"/>}/>. 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(

<>

<h1>Assignment 4</h1>

</>

);

};

export default Assignment4;

import { Link } from "react-router-dom";

function Nav() {

return (

<nav className="nav nav-pills mb-2">

<Link className="nav-link" to="/Labs/a3">

A3</Link>

<Link className="nav-link" to="/Labs/a4">

A4</Link>

<Link className="nav-link" to="/hello">

Hello</Link>

<Link 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 (

<div>

<Nav/>

<Routes>

<Route path="/"

element={<Navigate

Kanbas</Link>

</nav>

);

}

export default Nav;

to="a3"/>}/>

<Route path="a3"

element={<Assignment3/>}/>

<Route path="a4"

element={<Assignment4/>}/>

</Routes>

</div>

);

}

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 (

<div>

<h2>Click Event</h2>

<button onClick={hello}>

Click Hello</button>

<button onClick={() => lifeIs("Good!")}>

Click Good</button>

<button

onClick={() => {

hello();

lifeIs("Great!");

}}

>

Click Hello 3

</button>

</div>

);

}

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

<div>

<h2>Passing Data on Event</h2>

<button onClick={() => add(2, 3)}

// onClick={add(2, 3)}

className="btn btn-primary">

Pass 2 and 3 to add()

</button>

</div>

);

}

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 (

<div>

<h2>Passing Functions</h2>

<button onClick={theFunction} className="btn btn-primary">

Invoke the Function

</button>

</div>

);

}

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 (

<div>

<h1>Assignment 4</h1>

<PassingFunctions theFunction={sayHello} />

...

</div>

);

}

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 (

<div>

<h2>Event Object</h2>

<button id="event-button"

onClick={(e) => handleClick(e)}

className="btn btn-primary">

Display Event Object

</button>

<pre>{JSON.stringify(event, null, 2)}</pre>

</div>

);

}

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 (

<div>

<h2>Counter: {count}</h2>

<button

onClick={() => { count++; console.log(count); }}>

Up

</button>

<button

onClick={() => { count--; console.log(count); }}>

Down

</button>

</div>

);

}

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 (

<div>

<h2>Counter: {count}</h2>

<button onClick={() => setCount(count + 1)}>Up</button>

<button onClick={() => setCount(count - 1)}>Down</button>

</div>

);

}

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 (

<div>

<h2>Boolean State Variables</h2>

<p>{done ? "Done" : "Not done"}</p>

<label className="form-control">

<input type="checkbox" checked={done}

onChange={() => setDone(!done)} />

Done

</label>

{done && <div className="alert alert-success">

Yay! you are done</div>}

</div>

);

}

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 (

<div>

<h2>String State Variables</h2>

<p>{firstName}</p>

<input

className="form-control"

value={firstName}

onChange={(e) => setFirstName(e.target.value)}/>

</div>

);

}

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 (

<div>

<h2>Date State Variables</h2>

<h3>{JSON.stringify(startDate)}</h3>

<h3>{dateObjectToHtmlDateString(startDate)}</h3>

<input

className="form-control"

type="date"

value={dateObjectToHtmlDateString(startDate)}

onChange={(e) => setStartDate(new Date(e.target.value))}

/>

</div>

);

}

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 (

<div>

<h2>Object State Variables</h2>

<pre>{JSON.stringify(person, null, 2)}</pre>

<input

value={person.name}

onChange={(e) => setPerson({ ...person, name: e.target.value })}

/>

<input

value={person.age}

onChange={(e) => setPerson({ ...person,

age: parseInt(e.target.value) })}

/>

</div>

);

}

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 (

<div>

<h2>Array State Variable</h2>

<button onClick={addElement}>Add Element</button>

<ul>

{array.map((item, index) => (

<li key={index}>

{item}

<button onClick={() => deleteElement(index)}>

Delete</button>

</li>

))}

</ul>

</div>

);

}

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 (

<div>

<h2>Counter {counter}</h2>

<ChildStateComponent

counter={counter}

setCounter={setCounter} />

</div>

);

}

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 (

<div>

<h3>Counter {counter}</h3>

<button onClick={() => setCounter(counter + 1)}>

Increment</button>

<button onClick={() => setCounter(counter - 1)}>

Decrement</button>

</div>

);

}

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(

<div>

<h2>Redux Examples</h2>

</div>

);

};

export default ReduxExamples;

import React from "react";

import ReduxExamples from "./redux-examples";

const Assignment4 = () => {

return(

<>

<h1>Assignment 4</h1>

<ReduxExamples/>

...

</>

);

};

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 (

<Provider store={store}>

<div className="container-fluid">

<h1>Labs</h1>

...

</div>

</Provider>

);

}

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 (

<div>

<h1>Hello Redux</h1>

<h2>{message}</h2>

</div>

);

}

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 (

<div>

<h2>Counter Redux</h2>

<h3>{count}</h3>

<button onClick={() => dispatch(increment())}> Increment </button>

<button onClick={() => dispatch(decrement())}> Decrement </button>

</div>

);

}

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 reducer function as shown in the AddRedux component below. Maintain the

values of a and b as local component state variables, and then pass them to add as a single object.

src/Labs/a4/ReduxExamples/AddRedux/index.tsx

import { useSelector, useDispatch } from "react-redux";

import { useState } from "react";

import { add } from "./addReducer";

import { LabState } from "../../../store";

function AddRedux() {

const [a, setA] = useState(12);

const [b, setB] = useState(23);

const { sum } = useSelector((state: LabState) => state.addReducer);

const dispatch = useDispatch();

return (

<div className="w-25">

<h1>Add Redux</h1>

<h2>

{a} + {b} = {sum}

</h2>

<input

type="number"

value={a}

onChange={(e) => setA(parseInt(e.target.value))}

className="form-control"

/>

<input

type="number"

value={b}

onChange={(e) => setB(parseInt(e.target.value))}

className="form-control"

/>

<button

onClick={() => dispatch(add({ a, b }))}

className="btn btn-primary"

>

Add Redux

</button>

</div>

);

}

export default AddRedux;

// to read/write to reducer

// to maintain a and b parameters in UI

// a and b state variables to edit

// parameters to add in the reducer

// read the sum state variable from the reducer

// dispatch to call add redux function

// render local state variables a and b, as well

// as application state variable sum

// update the local component state variable a

// update the local component state variable b

// on click, call add reducer function to

// compute the arithmetic addition of a and b,

// and store it in application state

// variable sum

2.5 Implementing a Todo List

Let's practice using local component state as well as application level state to

implement a simple Todo List component. First we'll implement the component

using only component state with useState which will limit the todos to only

available within the Todo List. We'll then add application state support to

demonstrate how the todos can be shared with any component or screen in the

application. Create the TodoList component as shown below.

src/Labs/a4/ReduxExamples/todos/TodoList.tsx

import React, { useState } from "react";

function TodoList() {

const [todos, setTodos] = useState([

{ id: "1", title: "Learn React" },

{ id: "2", title: "Learn Node" }]);

// import useState

// create todos array state variable

// initialize with 2 todo objects

const [todo, setTodo] = useState({ id: "-1", title: "Learn Mongo" });

const addTodo = (todo: any) => {

const newTodos = [ ...todos, { ...todo,

id: new Date().getTime().toString() }];

setTodos(newTodos);

setTodo({id: "-1", title: ""});

};

const deleteTodo = (id: string) => {

const newTodos = todos.filter((todo) => todo.id !== id);

setTodos(newTodos);

};

const updateTodo = (todo: any) => {

const newTodos = todos.map((item) =>

(item.id === todo.id ? todo : item));

setTodos(newTodos);

setTodo({id: "-1", title: ""});

};

return (

<div>

<h2>Todo List</h2>

<ul className="list-group">

<li className="list-group-item">

<button onClick={() => addTodo(todo)}>Add</button>

<button onClick={() => updateTodo(todo)}>

Update </button>

<input

value={todo.title}

onChange={(e) =>

setTodo({ ...todo,

title: e.target.value })

}

/>

</li>

{todos.map((todo) => (

<li key={todo.id} className="list-group-item">

<button onClick={() => deleteTodo(todo.id)}>

Delete </button>

<button onClick={() => setTodo(todo)}>

Edit </button>

{todo.title}

</li>

))}

</ul>

</div>

);

}

export default TodoList;

// create todo state variable object

// event handler to add new todo

// spread existing todos, append new todo,

// override id

// update todos

// clear the todo

// event handler to remove todo by their ID

// event handler to

// update todo by

// replacing todo

// by their ID

// add todo button

// update todo button

// input field to update todo's title

// for every keystroke

// update the todo's title, but copy old

values first

// render all todos

// as line items

// button to delete todo by their ID

// button to select todo to edit

2.5.1 Breaking up Large Components

Let's break up the TodoList component into several smaller components: TodoItem and TodoForm. TodoItem shown below

breaks out the line items that render the todo's title, and Delete and Edit buttons. The component accepts references to

the todo object, as well as deleteTodo and setTodo functions.

src/Labs/a4/ReduxExamples/todos/TodoItem.tsx

function TodoItem({ todo, deleteTodo, setTodo }: {

todo: { id: string; title: string };

deleteTodo: (id: string) => void;

setTodo: (todo: { id: string; title: string }) => void;

}) {

return (

<li key={todo.id} className="list-group-item">

<button onClick={() => deleteTodo(todo.id)}> Delete </button>

<button onClick={() => setTodo(todo)}> Edit </button>

{todo.title}

</li>

);

}

export default TodoItem;

// breaks out todo item

// todo to render

// event handler to remove todo

// event handler to select todo

// invoke delete todo with ID

// invoke select todo

// render todo's title

Similarly we'll break out the form to Create and Update todos into component TodoForm shown below. Parameters todo,

setTodo, addTodo, and updateTodo, to maintain dependencies between the TodoList and TodoForm component.

src/Labs/a4/ReduxExamples/todos/TodoForm.tsx

function TodoForm({ todo, setTodo, addTodo, updateTodo }: {

todo: { id: string; title: string };

setTodo: (todo: { id: string; title: string }) => void;

addTodo: (todo: { id: string; title: string }) => void;

updateTodo: (todo: { id: string; title: string }) => void;

}) {

return (

<li className="list-group-item">

<button onClick={() => addTodo(todo)}> Add </button>

<button onClick={() => updateTodo(todo)}> Update </button>

<input

value={todo.title}

onChange={ (e) => setTodo({ ...todo, title: e.target.value }) }

/>

</li>

);

}

export default TodoForm;

// breaks out todo form

// todo to be added or edited

// event handler to update todo's title

// event handler to add new todo

// event handler to update todo

// invoke add new todo

// invoke update todo

// input field to update

// todo's title

// update title on each key stroke

Now we can replace the form and todo items in the TodoList component as shown below. Add the TodoList component to

Assignment4 and confirm it works as expected.

src/Labs/a4/ReduxExamples/todos/TodoList.tsx

import React, { useState } from "react";

import TodoForm from "./TodoForm";

import TodoItem from "./TodoItem";

function TodoList() {

...

return (

<div>

<h2>Todo List</h2>

<ul className="list-group">

<TodoForm

todo={todo}

setTodo={setTodo}

addTodo={addTodo}

updateTodo={updateTodo}/>

{todos.map((todo) => (

<TodoItem

todo={todo}

deleteTodo={deleteTodo}

setTodo={setTodo} />

))}

</ul>

</div>

);

}

export default TodoList;

// import TodoForm

// import TotoItem

// TodoForm breaks out form to add or update todo

// pass state variables and

// event handlers

// so component

// can communicate with TodoList's data and functions

// TodoItem breaks out todo item

// pass state variables and

// event handlers to

// communicate with TodoList's data and functions

2.5.2 Todos Reducer

Although the TodoList component might work as expected and it might be all we would need, it's implementation makes it

difficult to share the local state data (the todos) outside its context with other components or screens. For instance, how

would we go about accessing and displaying the todos, say, in the Assignment3 component or Kanbas? We would have to

move the todos state variable and mutator functions to a component that is parent to both the Assignment3 component

and the TodoList component, e.g., Labs.

Instead, let's move the state and functions from the TodoList component to a reducer and store so that the todos can be

accessed from anywhere within the Labs. Create todosReducer as shown below, moving the todos and todo state

variables to the reducer's initialState. Also move the addTodo, deleteTodo, updateTodo, and setTodo functions into the

reducers property, reimplementing them to use the state and action parameters of the new reducer functions.

src/Labs/a4/ReduxExamples/todos/todosReducer.ts

import { createSlice } from "@reduxjs/toolkit";

const initialState = {

todos: [

{ id: "1", title: "Learn React" },

{ id: "2", title: "Learn Node" },

],

todo: { title: "Learn Mongo" },

};

const todosSlice = createSlice({

name: "todos",

initialState,

reducers: {

addTodo: (state, action) => {

const newTodos = [

...state.todos,

{ ...action.payload, id: new Date().getTime().toString() },

];

state.todos = newTodos;

state.todo = { title: "" };

},

deleteTodo: (state, action) => {

const newTodos = state.todos.filter((todo) => todo.id !== action.payload);

state.todos = newTodos;

},

updateTodo: (state, action) => {

const newTodos = state.todos.map((item) =>

item.id === action.payload.id ? action.payload : item

);

state.todos = newTodos;

state.todo = { title: "" };

},

setTodo: (state, action) => {

state.todo = action.payload;

},

},

});

export const { addTodo, deleteTodo, updateTodo, setTodo } = todosSlice.actions;

export default todosSlice.reducer;

// import createSlice

// declare initial state of reducer

// moved here from TodoList.tsx

// todos has default todos

// todo has default todo

// create slice

// name slice

// configure store's initial state

// declare reducer functions

// addTodo reducer function, action

// contains new todo. newTodos

// copy old todos, append new todo

// in action.payload, override

// id as timestamp

// update todos

// clear todo

// deleteTodo reducer function,

// action contains todo's ID to

// filter out of newTodos

// updateTodo reducer function

// rebuilding newTodos by replacing

// old todo with new todo in

// action.payload

// update todos

// clear todo

// setTodo reducer function

// to update todo state variable

// export reducer functions

// export reducer for store

Add the new todosReducer to the store so that it can be provided to the rest of the Labs.

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";

import todosReducer from "../a4/ReduxExamples/todos/todosReducer";

export type TodoType = {

id: string;

title: string;

};

export interface LabState {

...

todosReducer: {

todos: TodoType[];

todo: TodoType;

};

}

const store = configureStore({

reducer: {

helloReducer,

counterReducer,

addReducer,

todosReducer,

},

});

export default store;

Now that we've moved the state and mutator functions to the todosReducer, refactor the TodoForm component to use the

reducer functions instead of the parameters. Also select the todo from the reducer state, instead of todo parameter.

src/Labs/a4/ReduxExamples/todos/TodoForm.tsx

import React from "react";

import { useSelector, useDispatch } from "react-redux";

import { addTodo, updateTodo, setTodo } from "./todosReducer";

import { LabState } from "../../../store";

function TodoForm(

{ todo,

setTodo,

addTodo,

updateTodo }

) {

const { todo } = useSelector((state: LabState) => state.todosReducer);

const dispatch = useDispatch();

return (

<li className="list-group-item">

<button onClick={() => dispatch(addTodo(todo))}> Add </button>

<button onClick={() => dispatch(updateTodo(todo))}> Update </button>

<input

value={todo.title}

onChange={(e) => dispatch(setTodo({ ...todo, title: e.target.value }))}

/>

</li>

);

}

export default TodoForm;

// import useSelector, useDispatch

// to read/write to reducer

// reducer functions

// remove dependency from

// parent component

// retrieve todo from reducer

// create dispatch instance to invoke

// reducer functions

// wrap reducer functions

// with dispatch

// wrap reducer functions

// with dispatch

Also reimplement the TodoItem component as shown below, using the reducer functions instead of the parameters.

src/Labs/a4/ReduxExamples/todos/TodoItem.tsx

import React from "react";

import { useDispatch } from "react-redux";

import { deleteTodo, setTodo } from "./todosReducer";

function TodoItem({ todo,

deleteTodo,

setTodo

}) {

const dispatch = useDispatch();

return (

<li key={todo.id} className="list-group-item">

<button onClick={() => dispatch(deleteTodo(todo.id))}> Delete </button>

<button onClick={() => dispatch(setTodo(todo))}> Edit </button>

{todo.title}

</li>

);

}

export default TodoItem;

// import useDispatch to invoke reducer

// functions deleteTodo and setTodo

// remove dependency with

// parent component

// create dispatch instance to invoke

// reducer functions

// wrap reducer functions with dispatch

Reimplement the TodoForm and TodoItem components as shown above and update the TodoList component as shown

below. Remove unnecessary dependencies and confirm that it works as before.

src/Labs/a4/ReduxExamples/todos/TodoList.tsx

import React from "react";

import TodoForm from "./TodoForm";

import TodoItem from "./TodoItem";

import { useSelector } from "react-redux";

import { LabState, TodoType } from "../../../store";

function TodoList() {

const { todos } = useSelector((state: LabState) => state.todosReducer);

return (

// import useSelector to retrieve

// data from reducer

// extract todos from reducer and remove

// all other event handlers

<div>

<h2>Todo List</h2>

<ul className="list-group">

<TodoForm />

{todos.map((todo: TodoType) => (

<TodoItem todo={todo} />

))}

</ul>

</div>

);

}

export default TodoList;

// remove unnecessary attributes

// remove unnecessary attributes,

// but still pass the todo

Now the todos are available to any component in the body of the Provider. To illustrate this, select the todos from within

the Assignment3 component as shown below and confirm the todos display in Assignment3.

src/Labs/a3/index.tsx

...

import { useSelector } from "react-redux";

import { LabState } from "../store";

function Assignment3() {

const { todos } = useSelector((state: LabState) => state.todosReducer);

return (

<div>

<h2>Assignment 3</h2>

<ul className="list-group">

{todos.map((todo) => (

<li className="list-group-item" key={todo.id}>

{todo.title}

</li>

))}

</ul>

...

</div>

);

}

export default Assignment3;

//

3 Implementing the Kanbas User Interface

The current Kanbas implementation reads data from a Database containing courses, modules, assignments, and grades,

and dynamically renders screens Dashboard, Home, Module, Assignments, and Grades. The data is currently static, and

our Kanbas implementation is basically a set of functions that transform the data in the Database into an corresponding

user interface. Since the data is static, the user interface is static as well. In this section we will use the component and

application state skills we learned in the Labs section, to refactor the Kanbas application so we can create new courses,

modules and assignments.

3.1 Dashboard

The current Dashboard implementation renders a static array of courses. Let's refactor the Dashboard so we can create

new courses, update existing course titles, and remove courses. Import the useState hook and convert the courses

constant into a state variable as shown below. Make these changes in your current implementation using the code below

as an example. The screenshot here on the right, gives an idea of the implementation suggested in the following

exercises. Use your existing HTML and CSS to render the courses with Bootstrap as you did for previous assignments.

Add a form similar to the one suggested here, as well as Edit and Delete buttons to each of the courses. Feel free to style

the new buttons and form as you like.

src/Kanbas/Dashboard/index.tsx

import React, { useState } from "react";

import { Link } from "react-router-dom";

import db from "../Database";

function Dashboard() {

const [courses, setCourses] = useState(db.courses);

return (

<div className="p-4">

<h1>Dashboard</h1> <hr />

<h2>Published Courses ({courses.length})</h2> <hr />

<div className="row">

<div className="row row-cols-1 row-cols-md-5 g-4">

{courses.map((course) => (

<div key={course._id} className="col" style={{ width: "300px" }}>

<div className="card">

... {course.name} ...

</div>

</div>

))}

</div>

</div>

</div>

);

}

export default Dashboard;

// add useState hook

// create courses state

// variable and initialize

// with database's courses

// use courses state variable instead of

// the database courses variable

// reuse the same HTML you used in

// previous assignments

3.1.1 Creating New Courses

To create new courses, implement addNewCourse function as shown below and new Add button that invokes

addNewCourse function to append a new course at the end of the courses array. The addNewCourse function overrides

the _id property with a unique timestamp. Confirm you can add new courses.

src/Kanbas/Dashboard/index.tsx

function Dashboard() {

const [courses, setCourses] = useState(db.courses);

const course = {

_id: "0", name: "New Course", number: "New Number",

startDate: "2023-09-10", endDate: "2023-12-15",

image: "/images/reactjs.jpg"

};

const addNewCourse = () => {

const newCourse = { ...course,

_id: new Date().getTime().toString() };

setCourses([...courses, { ...course, ...newCourse }]);

};

return (

<div>

<h1>Dashboard</h1>

<button onClick={addNewCourse} >

Add

</button>

...

</div>

);

}

// create a course object with default values

// create addNewCourse event handler that sets

// courses as copy of current courses state array

// add course at the end of the array

// overriding _id to current time stamp

// add button to invoke

// addNewCourse. Note no argument syntax

Use the course constant as the initial state of a new state variable of the same name as shown below. Add a form to edit

the course state variable's name, number, startDate, and endDate. Confirm form shows values of the course state variable.

src/Kanbas/Dashboard/index.tsx

function Dashboard() {

const [courses, setCourses] = useState(db.courses);

const [course, setCourse] = useState({

_id: "0", name: "New Course", number: "New Number",

startDate: "2023-09-10", endDate: "2023-12-15",

image: "/images/reactjs.jpg"

});

const addNewCourse = () => { ... };

return (

<div>

<h1>Dashboard</h1>

<h5>Course</h5>

<input value={course.name} className="form-control" />

<input value={course.number} className="form-control" />

<input value={course.startDate} className="form-control" type="date" />

<input value={course.endDate} className="form-control" type="date" />

<button onClick={addNewCourse} >

Add

</button>

</div>

);

}

// convert course into a state

// variable so we can change it

// and force a redraw of the UI

// add input element for each of

// fields in course state

// variable

Add onChange attributes to each of the input fields to update each of the fields using the setCourse mutator function, as

shown as below. Use your implementation of Dashboard and use the code provided as an example. Confirm you can add

edit and new courses.

src/Kanbas/Dashboard/index.tsx

function Dashboard() {

const [courses, setCourses] = useState(db.courses);

const [course, setCourse] = useState({ ... });

const addNewCourse = () => { ... };

return (

<div>

<h1>Dashboard</h1>

<h5>Course</h5>

<input value={course.name} className="form-control"

onChange={(e) => setCourse({ ...course, name: e.target.value }) } />

<input value={course.number} className="form-control"

onChange={(e) => setCourse({ ...course, number: e.target.value }) } />

<input value={course.startDate} className="form-control" type="date"

onChange={(e) => setCourse({ ...course, startDate: e.target.value }) }/>

<input value={course.endDate} className="form-control" type="date"

onChange={(e) => setCourse({ ...course, endDate: e.target.value }) } />

<button onClick={addNewCourse} >

Add

</button>

</div>

);

}

// add onChange event

// handlers to each input

// element to update

// course state with

// event's target value

3.1.2 Deleting a Course

Now let's implement deleting courses by adding Delete buttons to each of the courses. The buttons invoke a new

deleteCourse function that accepts the ID of the course to remove. The function filters out the course from the courses

array. Use the code below as an example to refactor your Dashboard component. Confirm that you can remove courses.

src/Kanbas/Dashboard/index.tsx

function Dashboard() {

const [courses, setCourses] = useState(db.courses);

const [course, setCourse] = useState({ ... });

const addNewCourse = () => { ... };

const deleteCourse = (courseId: string) => {

setCourses(courses.filter((course) => course._id !== courseId));

};

return (

<div>

<h1>Dashboard</h1>

<div className="row">

<div className="row row-cols-1 row-cols-md-5 g-4">

{courses.map((course) => (

...

<Link className="card-title"

to={`/Kanbas/Courses/${course._id}`}>

{course.name}

<button onClick={(event) => {

event.preventDefault();

deleteCourse(course._id);

}}>

Delete

</button>

</Link>

...

))}

</div>

</div>

</div>

);

}

export default Dashboard;

// add deleteCourse event handler accepting

// ID of course to remove by filtering out

// the course by its ID

// add Delete button next to the course's

// name to invoke deleteCourse when clicked

// passing the course's ID and preventing

// the Link's default behavior to navigate

// to Course Screen

3.1.3 Editing a Course

Now let's implement editing an existing course by adding Edit buttons to each of the courses which invoke a new

setCourse function that copies the current course into the course state variable, displaying the course in the form so you

can edit it. Refactor your Dashboard component using the code below as an example. Confirm that clicking Edit of a

course, copies the course into the form.

src/Kanbas/Dashboard/index.tsx

...

{courses.map((course) => (

<Link key={course._id}>

to={`/Kanbas/Courses/${course._id}`}

className="list-group-item">

<button onClick={(event) => {

event.preventDefault();

setCourse(course);

}}>

Edit

</button>

<button

onClick={(event) => {

event.preventDefault();

deleteCourse(course._id);

}}>

Delete

</button>

{course.name}

</Link>

))}

// add Edit button to copy the course to be

// edited into the form so we can edit it.

// prevent default to navigate to Course

// screen

Add a Update button to the form so that the selected course is updated with the values in the edited fields. Use the code

below as an example. Confirm you can select, and then edit the selected course.

src/Kanbas/Dashboard/index.tsx

function Dashboard() {

const [courses, setCourses] = useState(db.courses);

const [course, setCourse] = useState({ ... });

const updateCourse = () => {

setCourses(

courses.map((c) => {

if (c._id === course._id) {

return course;

} else {

return c;

}

})

);

};

return (

<div>

<h1>Dashboard</h1>

<h5>Course</h5>

<input value={course.name} className="form-control".../>

<input value={course.number} className="form-control".../>

<input value={course.startDate} className="form-control".../>

<input value={course.endDate} className="form-control".../>

<button onClick={addNewCourse} >

Add

</button>

<button onClick={updateCourse} >

Update

</button>

</div>

);

}

3.2 Courses Screen

The Dashboard component seems to be working fine, but the courses it is creating, deleting, and updating can not be used

outside of the component. This is a problem because the Courses screen would want to be able to render the new

courses, but it doesn't have access to the courses state variable in the Dashboard. To fix this we need to either add redux

so all courses are available anywhere, or move the courses state variable to a component that contains both the

Dashboard and the Courses. Let's take this last approach first, and then we'll explore adding Redux. Let's move all the state

variables and event handlers from the Dashboard, and move them to the Kanbas component since it is parent to both the

Dashboard and Courses component. Then add references to the state variables and event handlers as parameter

dependencies in Dashboard as shown below. Refactor your Dashboard component based on the example code below.

src/Kanbas/Dashboard/index.tsx

function Dashboard(

{ courses, course, setCourse, addNewCourse,

deleteCourse, updateCourse }: {

courses: any[]; course: any; setCourse: (course: any) => void;

addNewCourse: () => void; deleteCourse: (course: any) => void;

updateCourse: () => void; })

{

return (

<div>

<h1>Dashboard</h1>

</div>

); }

// move the state variables and

// event handler functions

// to Kanbas and then accept

// them as parameters

Refactor your Kanbas component moving the state variables and functions from the Dashboard component. Confirm the

Dashboard still works the same, e.g., renders the courses, can add, updates, and remove courses

src/Kanbas/index.tsx

import KanbasNavigation from "./KanbasNavigation";

import { Routes, Route, Navigate } from "react-router-dom";

import Dashboard from "./Dashboard";

import Courses from "./Courses";

import db from "./Database";

import { useState } from "react";

function Kanbas() {

const [courses, setCourses] = useState<any[]>(db.courses);

const [course, setCourse] = useState({

_id: "1234", name: "New Course", number: "New Number",

startDate: "2023-09-10", endDate: "2023-12-15",

});

const addNewCourse = () => {

setCourses([...courses, { ...course, _id: new Date().getTime().toString() }]);

};

const deleteCourse = (courseId: any) => {

setCourses(courses.filter((course) => course._id !== courseId));

};

const updateCourse = () => {

setCourses(

courses.map((c) => {

if (c._id === course._id) {

return course;

} else {

return c;

}

})

);

};

return (

<div className="d-flex">

<KanbasNavigation />

<div>

<Routes>

<Route path="/" element={<Navigate to="Dashboard" />} />

<Route path="Account" element={<h1>Account</h1>} />

<Route path="Dashboard" element={

<Dashboard

courses={courses}

course={course}

setCourse={setCourse}

addNewCourse={addNewCourse}

deleteCourse={deleteCourse}

updateCourse={updateCourse}/>

} />

<Route path="Courses/:courseId/*" element={

<Courses courses={courses} />} />

</Routes>

</div>

</div>

);

}

export default Kanbas;

// import the database

// import the useState hook

// move the state variables here

// from the Dashboard

// move the event handlers here

// from the Dashboard

// pass a reference of the state

// variables and event handlers to

// the Dashboard so it can read

// the state variables and invoke

// the event handlers from the

// Dashboard

// also pass all the courses to

// the Courses screen since now

// it might contain new courses

// not initially in the database

Now that we have the courses declared in the Kanbas component, we can share them with the Courses screen component

by passing them as an attribute. The Courses component destructs the courses from the parameter and then finds the

course by the courseId path parameter searching through the courses parameter instead of the courses in the Database.

Refactor your Courses component as suggested below and confirm you can navigate to new courses created in the

Dashboard.

src/Kanbas/Courses/index.tsx

...

function Courses({ courses }: { courses: any[]; }) { // accept courses from Kanbas

const { courseId } = useParams();

const course = courses.find((course) => course._id === courseId);

return (...);

}

export default Courses;

// find the course by its ID

3.3 Modules

Now let's do the same with Modules. We'll first refactor the ModuleList

component using component state variables so that we can create, update, and

remove modules. We'll discover the same limitation we had with courses, i.e., we

won't be able to share new modules outside the ModuleList. But instead of

moving the modules state variable and functions to a shared common parent

component, we'll instead use Redux to make the modules available throughout

the application. The screenshot here on the right is for illustration purposes only.

Reuse the HTML and CSS from previous assignments to style your modules.

Refactor your ModuleList implementation by converting the modules array into a state variable as shown below. Confirm

ModuleList renders as expected. Styling shown here is for illustration purposes. Use your HTML and CSS from previous

assignments to style the modules.

src/Kanbas/Courses/Modules/List.tsx

import React, { useState } from "react";

import { useParams } from "react-router-dom";

import { modules } from "../../Database";

function ModuleList() {

const { courseId } = useParams();

const [moduleList, setModuleList] = useState<any[]>(modules);

return (

<>

<ul className="list-group wd-modules">

{moduleList

.filter((module) => module.course === courseId)

.map((module, index) => (

<li key={index} className="list-group-item">

...

{module.name}

<p>{module.description}</p>

<p>{module._id}</p>

...

</li>))}

</ul>

</>

);

}

export default ModuleList;

// import useState to create

// state variables

// create modules state variables

// initialized from db

3.3.1 Creating a Module

Add a new module state variable and corresponding form to edit and

create new module names and titles as shown below. Refactor your

ModuleList component as suggested below and confirm the form renders

the module state variable as expected. Reuse the HTML and CSS from

previous assignments to style the modules. You can use the styling

suggested here for the form and buttons, but feel free to come up with

your own unique styling.

src/Kanbas/Courses/Modules/List.tsx

...

function ModuleList() {

const { courseId } = useParams();

const [moduleList, setModuleList] = useState<any[]>(modules);

const [module, setModule] = useState({

name: "New Module",

description: "New Description",

course: courseId,

});

return (

<>

<ul className="list-group wd-modules">

<li className="list-group-item">

<button>Add</button>

<input value={module.name}

onChange={(e) => setModule({

...module, name: e.target.value })}

/>

<textarea value={module.description}

onChange={(e) => setModule({

...module, description: e.target.value })}

/>

</li>

{moduleList

.filter((module) => module.course === courseId)

.map((module, index) => (

<li key={index} className="list-group-item">

...

</li>))}

</ul>

</>

);

}

export default ModuleList;

// declare module state variable initialized with

// default values for name, description, and course

// used to edit new and existing modules

// add a form to edit the module

// Add button to add the new module

// input field to edit module's name. default

// value from module.name. update module.name for

// every key stroke

// textarea to edit module's description. default

// value from module.description. update description

// for every key stroke

Implement a new addModule function that appends a new module at the end of the modules state

variable. Confirm you can add new modules. Reuse the HTML and CSS from previous

assignments to style the modules. You can use the styling suggested here for the form and

buttons, but feel free to come up with your own unique styling.

src/Kanbas/Courses/Modules/List.tsx

function ModuleList() {

...

const [module, setModule] = useState({

_id: "0", name: "New Module",

description: "New Description",

course: courseId || "",

});

const addModule = (module: any) => {

const newModule = { ...module,

_id: new Date().getTime().toString() };

const newModuleList = [newModule, ...moduleList];

setModuleList(newModuleList);

};

return (

<>

<ul className="list-group wd-modules">

<li className="list-group-item">

<button onClick={() => { addModule(module) }}>

Add

</button>

...

</li>

...

</ul>

);

}

export default ModuleList;

// addModule appends new module at beginning of

// modules, overriding _id with a timestamp

// Add button calls addModule with module being

// edited in the form to be added to the modules

3.3.2 Deleting a Module

Add Delete buttons to each module that invokes a new deleteModule

function passing the ID of the module we want to remove. The new function

should filter out the module and create a new array without the module we

are deleting. Refactor ModuleList as suggested below and confirm you can

remove modules. Styling shown here is for illustration purposes. Use your

HTML and CSS from previous assignments to style the modules.

src/Kanbas/Courses/Modules/List.tsx

function ModuleList() {

...

const [module, setModule] = useState({ ... });

const addModule = () => { ... };

const deleteModule = (moduleId: string) => {

const newModuleList = moduleList.filter(

(module) => module._id !== moduleId );

setModuleList(newModuleList);

};

return (

<>

<ul className="list-group wd-modules">

...

{moduleList

.filter((module) => module.course === courseId)

.map((module, index) => (

<li key={index} className="list-group-item">

<button

onClick={() => deleteModule(module._id)}>

Delete

</button>

...

{module.name}

<p>{module.description}</p>

...

</li>

...

</ul>

);

}

export default ModuleList;

// deleteModule filters out the module whose ID is

// equal to the parameter moduleId

// delete button calls deleteModule with module's ID

// to be removed

3.3.3 Editing a Module

Add an Edit button to each of the modules that copies the corresponding module to

the form as shown below. Also add a new Update button to the form which computes

a new modules array that replaces the module being edited with the updates in the

form. Confirm you can edit modules. Styling shown here is for illustration purposes.

Use your HTML and CSS from previous assignments to style the modules.

src/Kanbas/Courses/Modules/ModuleList.tsx

function ModuleList() {

const [module, setModule] = useState({ ... });

...

const updateModule = () => {

const newModuleList = moduleList.map((m) => {

if (m._id === module._id) {

return module;

} else {

return m;

}

});

setModuleList(newModuleList);

};

return (

<ul className="list-group">

<li className="list-group-item">

<button onClick={addModule}>Add</button>

<button onClick={updateModule}>

Update

</button>

...

</li>

{moduleList

.filter((module) => module.course === courseId)

.map((module, index) => (

<li key={index} className="list-group-item">

<button

onClick={(event) => { setModule(module); }}>

Edit

</button>

<button

onClick={() => deleteModule(module._id)}>

Delete

</button>

{module.name}

...

</li>))}

</ul>

);

}

export default ModuleList;

// updateModule rebuilds modules by replacing the module

// whose ID matches the current module being edited

// update button calls updateModule

// edit button copies this module to current module

// so it can be edited

3.3.4 Module Reducer

The ModuleList seems to be working as expected being able to create new modules, edit modules, and remove modules,

BUT, it suffers a major flaw. Those new modules and edits can't be used outside the confines of the ModuleList

component even though we would want to display the same list of modules elsewhere such as the Home screen. We

could use the same approach as we did for the Dashboard, by moving the state variables and functions to a higher level

component that could share the state with other components. Instead we're going to use Redux this time to practice

application level state management. To start, create the moduleReducer.tsx shown below containing the modules and

module state variables as well as the addModule, deleteModule, updateModule, and setModule functions reimplemented

in the reducers property.

src/Kanbas/Courses/Modules/reducer.ts

import { createSlice } from "@reduxjs/toolkit";

import { modules } from "../../Database";

const initialState = {

modules: modules,

module: { name: "New Module 123", description: "New Description" },

};

const modulesSlice = createSlice({

name: "modules",

initialState,

reducers: {

addModule: (state, action) => {

state.modules = [

{ ...action.payload, _id: new Date().getTime().toString() },

...state.modules,

];

},

deleteModule: (state, action) => {

state.modules = state.modules.filter(

(module) => module._id !== action.payload

);

},

updateModule: (state, action) => {

state.modules = state.modules.map((module) => {

// import createSlice

// import modules from database

// create reducer's initial state with

// default modules copied from database

// default module

// create slice

// name the slice

// set initial state

// declare reducer functions

// new module is in action.payload

// update modules in state adding new module

// at beginning of array. Override _id with

// timestamp

// module ID to delete is in action.payload

// filter out module to delete

// module to update is in action.payload

// replace module whose ID matches

if (module._id === action.payload._id) {

return action.payload;

} else {

return module;

}

});

},

setModule: (state, action) => {

state.module = action.payload;

},

},

});

export const { addModule, deleteModule,

updateModule, setModule } = modulesSlice.actions;

export default modulesSlice.reducer;

// action.payload._id

// select the module to edit

// export all reducer functions

// export reducer

The reducers, store, and Provider we worked on for the Labs only wrapped the lab exercises, so those won't be available

here in Kanbas. Instead, let's create a new store and Provider specific for the Kanbas application. Create a new store as

shown below.

src/Kanbas/store/index.ts

import { configureStore } from "@reduxjs/toolkit";

import modulesReducer from "../Courses/Modules/reducer";

export interface KanbasState {

modulesReducer: {

modules: any[];

module: any;

};

}

const store = configureStore({

reducer: {

modulesReducer

}

});

export default store;

// configure a new store

// import reducer

// add reducer to store

Then provide the store to the whole Kanbas application as shown below.

src/Kanbas/index.tsx

...

import store from "./store";

import { Provider } from "react-redux";

function Kanbas() {

...

return (

<Provider store={store}>

<div className="d-flex">

<KanbasNavigation />

<div>

...

</div>

</Provider>

);

}

export default Kanbas;

// import the redux store

// import the redux store Provider

// wrap your application with the Provider so all

// child elements can read and write to the store

Reimplement the ModuleList by removing the state variables and functions, and replacing them with selectors,

dispatchers, and reducer functions as shown below. Confirm you can still add, remove, and edit modules as before.

src/Kanbas/Courses/Modules/List.tsx

import React, { useState } from "react";

import { useParams } from "react-router-dom";

import { useSelector, useDispatch } from "react-redux";

import {

addModule,

deleteModule,

updateModule,

setModule,

} from "./reducer";

import { KanbasState } from "../../store";

function ModuleList() {

const { courseId } = useParams();

const moduleList = useSelector((state: KanbasState) =>

state.modulesReducer.modules);

const module = useSelector((state: KanbasState) =>

state.modulesReducer.module);

const dispatch = useDispatch();

return (

<ul className="list-group">

<li className="list-group-item">

<button

onClick={() => dispatch(addModule({ ...module, course: courseId }))}>

Add

</button>

<button

onClick={() => dispatch(updateModule(module))}>

Update

</button>

<input

value={module.name}

onChange={(e) =>

dispatch(setModule({ ...module, name: e.target.value }))

}/>

<textarea

value={module.description}

onChange={(e) =>

dispatch(setModule({ ...module, description: e.target.value }))

}/>

</li>

{moduleList

.filter((module) => module.course === courseId)

.map((module, index) => (

<li key={index} className="list-group-item">

<button

onClick={() => dispatch(setModule(module))}>

Edit

</button>

<button

onClick={() => dispatch(deleteModule(module._id))}>

Delete

</button>

<h3>{module.name}</h3>

<p>{module.description}</p>

</li>

))}

</ul>

);

}

export default ModuleList;

// import useSelector and useDispatch

// import reducer functions to add,

// delete, and update modules

// retrieve current state variables

// modules and module from reducer

// get dispatch to call reducer

// functions

// wrap reducer functions with

// dispatch

// wrap reducer functions with

// dispatch

// wrap reducer functions with

// dispatch

// wrap reducer functions with

// dispatch

// wrap reducer functions with

// dispatch

// wrap reducer functions with

// dispatch

3.4 Assignments (graduates only)

After completing the Dashboard, Courses, and ModuleList, refactor the

Assignments and AssignmentEditor screens to create, update, and remove

assignments as described in this section.

3.4.1 Assignments Reducer

Following Modules/reducer.ts as an example, create an assignmentsReducer.ts in src/Kanbas/Courses/Assignments/

initialized with db.assignments. Implement reducer functions addAssignment, deleteAssignment, updateAssignment, and

selectAssignment. Add the assignmentsReducer to the store in Kanbas/store/index.ts to add the assignments to the

Kanbas application state

3.4.2 Creating an Assignment

Refactor your Assignments component as follows

● Clicking the + Assignment button navigates to the

AssignmentEditor screen

● The AssignmentEditor should allow editing the following fields:

name, description, points, dueDate, availableFromDate,

availableUntilDate.

● Clicking Save creates the new assignment and adds it to the

assignments array state variable and displays in the Assignments

screen which must now contain the newly created assignment.

● Clicking Cancel does not create the new assignment, and navigates

back to the Assignments screen, without the new assignment.

3.4.3 Editing an Assignment

Refactor the AssignmentsEditor component as follows

● Clicking on an assignment in the Assignments screen navigates to

the AssignmentsEditor screen, displaying the corresponding assignment.

● The AssignmentsEditor screen should allow editing the same fields listed earlier for corresponding assignment.

● Clicking Save updates the assignment's fields and navigates back to the Assignments screen with the updated

assignment values

● Clicking Cancel does not update the assignment, and navigates back to the Assignments screen

3.4.4 Deleting an Assignment

Refactor the Assignments component as follows

● Add a Delete button to the right of each assignment.

● Clicking Delete on an assignment pops up a dialog asking if you are sure you want to remove the assignment

● Clicking Yes or Ok, dismisses the dialog, removes the assignment, and updates the Assignments screen without

the deleted assignment.

● Clicking No or Cancel, dismisses the dialog without removing the assignment

4 Deliverables

As a deliverable, make sure you complete the Labs and Kanbas sections of this assignment. All your work must be done in

a branch called a4. When done, add, commit and push the branch to GitHub. Deploy the new branch to Netlify and confirm

it's available in a new URL based on the branch name. Submit the link to your GitHub repository and the new URL where

the branch deployed to in Netlify. Here's an example on the steps:

Create a branch called a4

git checkout -b a4

# do all your work

Do all your work, e.g., Labs exercises, Kanbas

Add, commit and push the new branch

git add .

git commit -am "a4 State and Redux fa23"

git push

If you have Netlify configured to auto deploy, then confirm it auto deployed. If not, then deploy the branch manually.

In Canvas, submit the following

1. The new URL where your a4 branch deployed to on Netlify

2. The link to your new branch in GitHub


相关文章

版权所有:编程辅导网 2021 All Rights Reserved 联系方式:QQ:99515681 微信:codinghelp 电子信箱:99515681@qq.com
免责声明:本站部分内容从网络整理而来,只供参考!如有版权问题可联系本站删除。 站长地图

python代写
微信客服:codinghelp