<info 340/>

Interactive React

Tim Carlson
Winter 2024

View of the Day

  • Quick Review: Chat Components

  • React Events

  • Managing State

  • Working with Forms

Project Draft 2

What we are looking for: Refactored Draft 1 into a React App 

Converted the HTML/CSS from draft 1 into a published React app. Began to add interactive functionality.

  • App is built: Ran create-react-app, etc. See assignment for details
  • ~90% of content rendered: "most" of your app should be there (main page at least, Components for everything else)
  • Has Components w/ props and data: Organize your Components! Can hard-code sample data for now
  • Has 1 feature almost implemented: Includes event handling and state manipulation
  • Fixes issues from draft 1: You're revising the HTML/CSS, fix problems while you're at it!
  • Published to Firebase hosting: get that working this draft
    (see assignment for details; demo on Tuesday)

Our chat app so far...

Demo overview: Components

Demo overview: Implementing Chat using React

function App(props) {

  //... do stuff
    
    return (
        // return a container with HeaderBar, ChannelList and ChatPane)
    )}

function ChannelNav(props) {  
  
  //Map (transform) an array of channel names into an array of <li> items
  
  return (
    // then return the li items inside of a <ul> wrapped in a <nav> element
  )}

function HeaderBar(props) {
 
  return (
    //a <header> element with the elements for the top header 
  )}

function ChatPane(props) {

  return(
    // take in an array of chat objects as a prop. filter that to reduce to the objects we want displayed, then 
    // sort the array to deceending order by datethen map the filtered array transforming into an array of <messages>
  )
}

function MessageItem(props) {
	// take in a messageObj as a prop, and decompose the needed fields (userName, userImage, text) into variables
	// to be used when creating the <message> element
    return (  
      // return a div that contains the image <img> and two paragraphs <p> along with a button <btn> with onClick()
	)
}

Demo overview: index.js

import React from 'react';
import ReactDOM from 'react-dom/client';

//css!
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';

import App from './components/App';


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Demo overview: App.js

import React from 'react';

import { HeaderBar } from './HeaderBar.js';
import { ChannelList } from './ChannelNav.js';
import { ChatPane } from './ChatPane.js';
import { ComposeForm } from './ComposeForm';

//A component!
export default function App(props) {

  const channelList = [
    'general', 'random', 'dank-memes', 'channel-4', 'pet-pictures' ]
  const currentChannel = "general";

  return (
    <div className="container-fluid d-flex flex-column">
      <HeaderBar />
      <div className="row flex-grow-1">
        <div className="col-3">
          <ChannelList channels={channelList} currentChannel={currentChannel} />
        </div>
        <div className="col d-flex flex-column">
          <ChatPane currentChannel={currentChannel} />
          <ComposeForm />
        </div>
      </div>
    </div>
  );
}

Demo overview: ChannelList and HeaderBar

import React from 'react';

export function ChannelList(props) {
  const channels = props.channels;
  const currentChannel = props.currentChannel;
  
  const linkElemArray = channels.map((channelNameString) => {

    let classList = "btn btn-outline-light my-1";
    if(channelNameString === currentChannel) {
      classList = "btn btn-warning"
    }

    const element = (
      <div key={channelNameString}>
        <button className={classList} href="">{channelNameString}</button>
      </div>
    )
    return element;
  })

  return (
    <nav className="text-light bg-secondary h-100 py-3 channel-nav px-2">
      {linkElemArray}
    </nav>
  )
}
import React from 'react';

export function HeaderBar(props) {
  return (
    <div className="text-light bg-primary p-1">
      <h1>React Chat</h1>
    </div>
  )
}

Demo overview: ChatPane (in ChatPane.js)

import CHAT_HISTORY from '../data/chat_log.json';

export function ChatPane(props) {
  const currentChannel = props.currentChannel;

  const handleClick = (event) => {
    console.log("you clicked me!");
  }

  //only show current channel messages
  const channelMessage = CHAT_HISTORY.filter((msgObj) => {
    return msgObj.channel === currentChannel;
  })

  const messageItemArray = channelMessage.map((messageObj) => {
    const element = (
      <MessageItem 
        messageData={messageObj} 
        key={messageObj.timestamp} 
      />
    )
    return element;
  })

  return (
    <div className="scrollable-pane">
      <div className="pt-2 my-2">
        {/* testing button */}
        <button className="btn btn-outline-primary mb-3" onClick={handleClick}>
          Click me!
        </button>
        <hr/>
        {messageItemArray}
      </div>
    </div>
  )
}

Demo overview: MessageItem (in ChatPane.js)

function MessageItem(props) {
  const {userName, userImg, text} = props.messageData;
  // const userName = props.messageData.userName;
  // const userImg = props.messageData.userImg;

  const handleClick = (event) => {
    console.log("you liked "+userName+"'s post!");
  }

  return (
    <div className="message d-flex mb-3">
      <div className="me-2">
        <img src={userImg} alt={userName+"'s avatar"} />
      </div>
      <div className="flex-grow-1">
        <p className="user-name">{userName}</p>
        <p>{text}</p>
        <button className="btn like-button" onClick={handleClick}>
          <span className="material-icons" style={{ color: "grey" }}>favorite_border</span>
        </button>
      </div>
    </div>
  )
}

Concepts we'll be covering

 

  • Events  (like onclick, onfocus, on change, etc)
     

  • Creating event callback function (using arrow functions)
     

  • State and State vs Props
     

  • How to change state (using the the setState method from the 2nd argument of the stateArray)
     

  • Implication that setState() is asynchronous
     

  • Lifting State (Example - how does the tasklist know to refresh when task is added to task ( a sibling) form?)

 

Conditional Rendering

You can use control logic (if statements) to specify whether or not a component should be rendered.

function ConditionalPanel(props) {
  //assign element to show to variable
  let thingToRender = null; //null element will not render
  if(conditionOne){ //based on props or state
    thingToRender = <OptionA />
  } else if(conditionTwo) {
    thingToRender = <OptionB />
  } else if(conditionThree) {
    return null; //show nothing!
  }
    
  //keep return statement as simple as possible!
  return (<div>{thingToRender}</div>);
}
function ConditionPanel(props) {
  //can use inline expressions via shortcutting. Use with caution
  return (
    <div>
      {conditionOne == true && <OptionA />}
    </div>
  )
}

Conditional Rendering

export function ChatPane(props) {
...

  const messageItemArray = channelMessages.map((messageObj) => {
    const element = (
      <MessageItem 
        messageData={messageObj} 
        key={messageObj.timestamp} 
      />
    )
    return element;
  })

  if (channelMessages.length === 0) {
    return <p>No Messages Yet</p>
  }

   return (
    <div className="scrollable-pane">
      <div className="pt-2 my-2">
        {currentChannel === 'general' &&
          <div>
            <button className="btn btn-outline-primary mb-3" onClick={handleClick}>
              Click me: {currentCount}
            </button>
            <hr />
          </div>}
        {messageItemArray}
      </div>
    </div>
  )

If there are no messages, return "No Messages Yet" rather than the empty array below

That weird conditional syntax from slide 13

React Events

We add user interaction in React the same way as with the DOM: by listening for events and executing callback functions when they occur. 

function MyButton() {
  //A function that will be called when clicked 
  //The name is conventional, but arbitrary.
  //The callback will be passed the DOM event as usual
  const handleClick = function(event) {
    console.log("clicky clicky");
  }

  //make a button with an `onClick` attribute!
  //this "registers" the listener and sets the callback
  return <button onClick={handleClick}>Click me!</button>;
}

special React prop

can only put listeners on HTML
elements, not Components!

function MyButton(props) {
  //A function that will be called when clicked 
  //The name is conventional, but arbitrary.
  //The callback will be passed the DOM event as usual
  const handleClick = (event) => {
    console.log("clicky clicky");
  }

  //make a button with an `onClick` attribute!
  //this "registers" the listener and sets the callback
  return <button onClick={handleClick}>Click me!</button>;
}

React Events

let currentCount = 0;
  
  const handleClick = (event) => {
    console.log("you clicked me!");
    currentCount = currentCount + 1;
  }
 

This local variable (currentCount) won't work because it doesn't cause the component to re-render

Also can't store it in a prop because they are read only (next slide)

React Props

A Component's props are information received from the "outside" that describe that component. They are inputs only, and are constants that are not changed over time

props are a Component's configuration, its options if you may. They are received from above and immutable as far as the Component receiving them is concerned. A Component cannot change its props, but it is responsible for putting together the props of its child Components.

In addition to the props, React components can also track their internal state. This keeps track of information about the Component that may change due to user interaction.

React State

State is reserved only for interactivity, that is, data that changes over time

Some examples of state data:

  1. The sorted order of child components
  2. Timers or dynamic content
  3. Which model data are shown!

DOM Interactivity

//The current "state"
const state = {
  data: [ {}, {}, {} ],
  ...
}

  
//define presentation - lots of these kinds of functions
function renderData() {
  //render all the data based on the state variable
  //...
}

//define user interaction
button.addEventListener('click', function() {
  //MODIFY THE STATE
  state.data[i] = ...; 

  //CLEAR OLD VIEW AND RE-RENDER CONTENT
  document.querySelector('#main').innerHTML = '';
  renderData(); //RE-RENDER CONTENT    
})

changeable data lives out here

1. modify the state data

2. re-render the view

2. re-render the view

On button click, do 2 things:

You add state to a component by using a state hook. The hook defines a "state variable" which will retain its value across Component function calls, as well as a function to update that variable.

Using State Hooks

//import the state hook function `useState()` to define state
import React, { useState } from 'react';

function CountingButton(props) {
  const [count, setCount] = useState(0);

  
  
 
  const handleClick = (event) => {
    setCount(count+1); //update the state to be a new value
                       //and RE-RENDER the Component!
  }

  return (
      <button onClick={handleClick}>Clicked {count} times</button>
  );
}

state variable

update function

initial value for variable

Using State Hooks

  const [count, setCount] = useState(0);
  const currentChannel = props.currentChannel;
  
  const handleClick = (event) => {
    console.log("you clicked me!");
    setCount(count + 1);
  }

if (channelMessages.length === 0) {
  return <p>No Messages Yet</p>
}

...

return (
    <div className="scrollable-pane">
      <div className="pt-2 my-2">
        {/* testing button */}
        {currentChannel === 'general' &&
          <div>
            <button className="btn btn-outline-primary mb-3" onClick={handleClick}>
              Click me: {count}
            </button>
            <hr />
          </div>}
        {/* messages */}
        {messageItemArray}
      </div>
    </div>
  )
}

state variable and state Setter

render the current state variable

setState updates the value and rerenders the component

Using State Hooks

function MessageItem(props) {
const { userName, userImg, text } = props.messageData;

const [isLiked, setIsLiked ] = useState(false);

const handleClick = (event) => {

  setIsLiked(!isLiked);
  console.log("you liked " + userName + "'s post!");
}

let heartColor = 'grey';
if (isLiked){
  heartColor = 'red';
}

return (
  <div className="message d-flex mb-3">
    <div className="me-2">
      <img src={userImg} alt={userName + "'s avatar"} />
    </div>
    <div className="flex-grow-1">
      <p className="user-name">{userName}</p>
      <p>{text}</p>
      <button className="btn like-button" onClick={handleClick}>
        <span className="material-icons" style={{ color: heartColor }}>favorite_border</span>
      </button>
    </div>
  </div>
)
}

state variable and state Setter

render the current state variable

setState updates the value and rerenders the component

Naming Conventions Matter!

In order to write correct React (that can be understood
and debugged by you and others), you need to follow the naming conventions:

  • The argument to a Component function is called props (with an s)
     
  • A "state-setter" function for state variable foo is called setFoo (replacing "foo" with the state variable name)

Changing State

React state is changed asynchronously.
Calling a "state-setter" function automatically re-renders the Component (by calling the function again).

function CountingButton(props) {
  const [count, setCount] = useState(3) //initial value of 3

  console.log(count); //will have "current" value of state
                    //3 first render, 4 after clicking
  
  const handleClick = (event) => {
    setCount(4); //request to change `count` to 4 AND re-render!
    console.log(count); //will output "3"; 
                        //state has not changed yet!
  }  
 
  return (
    <button onClick={handleClick}>Clicked {count} times</button>
  );    
}

Debugging State

Because state changes are asynchronous, you can only "see" them after the component has re-rendered. Use console logs at the "rendering" step to debug 

function CountingButton(props) {
  const [count, setCount] = useState(3) //initial value of 3

  console.log("DEBUG: count", count); //debug! variable here, 
                                      //after re-render
  
  
  const handleClick = (event) => {
    setCount(count + 1); //increment count AND re-render!
    //do not debug variable here!
  }  
  
  return (
    <button onClick={handleClick}>Clicked {count} times</button>
  );    
}

Multiple State Variables

Components can (and often do) contain multiple state variables.

//Example from React documentation
function ExampleWithManyStates(props) {
  //Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  
  //...  
}

state variable is an array of objects!

State & Arrays/Objects

State variables will only be updated if a different value is passed to the setter function. For arrays and objects, pass a copy of the element with an updated element or property.

function TodoListWithError(props) {
  //a state value that is an array of objects
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

  const handleClick = (event) => {
    todos[0].text = "Fix bugs"; //modify the object 
                                //but don't make a new one
    setTodos(todos) //This won't work! Not "changing"
  }
  
  //...
}
function TodoList(props) {
  //a state value that is an array of objects
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

  const handleClick = (event) => {
    //create a copy of the array using the `map()` function
    const todosCopy = todos.map((todoObject, index) => {
      const objCopy = {...todoObject}; //copy object as well
      if(index == 0) { //transform objects if needed
        objCopy.text = "Fix bugs"
      }
      return objCopy; //return object to go into new array
    })
    setTodos(todosCopy) //This works!
  }
  
  //...
}

Props vs State

props are for information that doesn’t change from the Component’s perspective, including “initial” data. state is for information that will change, usually due to user interaction (see React FAQ).

  1. Is the value passed in from a parent via props? If so, it probably isn’t state.
     
  2. Does the value remain unchanged over time? If so, it definitely isn’t state.
     
  3. Can you compute it based on any other state or props in your component? If so, it definitely isn’t state.

props are for information that doesn’t change from the Component’s perspective, including “initial” data. state is for information that will change, usually due to user interaction (see React FAQ).

React Form Inputs

To access the value in an <input>, save that value in the state (and update it onChange). This is called a controlled form.

use DOM event to refer to which input element

function MyInput(props) {
  const [inputValue, setInputValue] = useState('')
  
  const handleChange = (event) => {
    let newValue = event.target.value
    setInputValue(newValue);
  }

  return (
    <div>
      <input type="text" onChange={handleChange} value={inputValue} />
      You typed: {value}
    </div>);
  )
}

ALL FORMS MUST BE CONTROLLED

React Form Inputs

// import React from 'react';
import React, { useState } from 'react';
export function ComposeForm(props) {

  const [typedValue, setTypedValue] = useState("");

  const handleChange = (event) => {
    const value = event.target.value;
    console.log("user typed:", value);
    setTypedValue(value); //update state and rerender
  }

  const handleSubmit = (event) => {
    console.log("submit the form");
    console.log("post", typedValue)
  }

  return (
    <form className="my-2">
      <div className="input-group">
        <textarea
          className="form-control" rows="2"
          placeholder="Type a new message"
          value={typedValue}
          onChange={handleChange}>
        </textarea>
        <button className="btn btn-secondary" type="button" onClick={handleSubmit} >
          <span className="material-icons">send</span>
        </button>
      </div>
    </form>
  );

add 'value' key for controlled form

add 'onChange' key for controlled form

state variable to keep track

of text typed into the form

update the state var with the value we get back in the event prop

Lifting Up State

If multiple components rely on the same data (variable), you should "lift up" that state to a shared parent, who can pass the information back down as props.

ChildA

ChildB

Parent

Has Data
(state)

Needs Data

<ChildA data={data} />

Has Data (prop)

Has Data (prop)

<ChildA data={data} />

Has Data (state)

Passing Callbacks

function App(props) {
  const [data, setData] = useState([]);
  
  const addItemToData = (newItem) => {
    const newData = [...data, newItem]; //copy via spread
    setData(newData); //update state
  }

  return (
    <FruitButton callback={addItemToData} text={"Apple"} />
    <FruitButton callback={addItemToData} text={"Banana"} />
  )
}

function FruitButton(props) {
  //what to do when clicked
  const handleClick = (event) => {
    //call given callback, passing in given text
    props.callback(props.text);   
  }

  return (
    <button onClick={handleClick}>{props.text}</button>
  )
}

To allow child components to "update" the parent's state, pass them a callback function as a prop.
Style Guide: do not pass a state setter function directly.

Move State to App Example

ComposeForm

App

< ComposeForm

addMessageCallback={addMessage} />

ChatPane

< ChatPane

currentChannel={currentChannel}

chatMessages={chatMessages} />

export default function App(props {

  const [chatMessages, setChatMessages] = useState(CHAT_HISTORY);

  const addMessage = (text) => {
    const newMessage = { "userId": "penguin", "text": text, "timestamp": Date.now()... }
    const updateChatMessages = [...chatMessages, newMessage];
    setChatMessages(updateChatMessages);
  }
  
  ...
//Slide 35 - Elevate the Messages up to the App so both the ChatPane and the ComposeForm have access
import CHAT_HISTORY from '../data/chat_log.json';
export default function App(props) {

  const [chatMessages, setChatMessages] = useState(CHAT_HISTORY);

  const addMessage = (text) => {
    const newMessage = { "userId": "penguin", "userName": "Penguin", "userImg": "/img/Penguin.png", 
                        "text": text, "timestamp": Date.now(), "channel": "general"
    }
    const updateChatMessages = [...chatMessages, newMessage];
    setChatMessages(updateChatMessages);
  }

...
return (
    <div className="container-fluid d-flex flex-column">
      <HeaderBar />
      <div className="row flex-grow-1">
        <div className="col-3">
          <ChannelList channels={channelList} currentChannel={currentChannel} />
        </div>
        <div className="col d-flex flex-column">
          <ChatPane 
            currentChannel={currentChannel}
            chatMessages={chatMessages} />
          <ComposeForm 
          addMessageCallback={addMessage}/>
        </div>
      </div>
    </div>
  );
}

App Update

Elevated State to App

callback to update state needs to reside here too

pass appropriate prop to child component

//Slide 35 - Elevate the Messages up to the App so both the ChatPand and the ComposeForm have access
export function ChatPane(props) {

// const [chatMessages, setChatMessages] = useState(CHAT_HISTORY);
  const currentChannel = props.currentChannel;

  const channelMessages = props.chatMessages.filter((msgObj) => {
    return msgObj.channel === currentChannel;
  })

  const messageItemArray = channelMessages.map((messageObj) => {
    const element = (
      <MessageItem messageData={messageObj} key={messageObj.timestamp} />
    )
    return element;
  })

...

return (
  <div className="scrollable-pane">
    <div className="pt-2 my-2">
      <button className="btn btn-outline-primary mb-3" onClick={handleClick}>
        Click me: {count}
      </button>
      <hr />

      {messageItemArray}
    </div>
  </div>
)
}

ChatPane Update

access the messages from prop

import React, { useState } from 'react';

export function ComposeForm(props) {
    const [typedValue, setTypedValue] = useState("");

  const handleChange = (event) => {
    const value = event.target.value;
    setTypedValue(value); //update state and rerender
  }

  const handleSubmit = (event) => {
    console.log("submit the form");
    console.log("post", typedValue);
    props.addMessageCallback(typedValue);
  }

  return (
    <form className="my-2">
      <div className="input-group">
        <textarea
          className="form-control" rows="2"
          placeholder="Type a new message"
          value={typedValue}
          onChange={handleChange}>
        </textarea>
        <button className="btn btn-secondary" type="button" onClick={handleSubmit} >
          <span className="material-icons">send</span>
        </button>
      </div>
    </form>
  );
}

ComposeForm Update

access the update message function from prop

Action Items!

Action Items!

  • Read/Review Ch 16-17: React & Interactive React

  • Problem Set 07 due Friday Feb 23

    • Get it done this week/weekend!

  • Project Draft 2 due Wednesday Feb 28

    • Convert Draft 1 into React Components!
    • Add one interactive feature

 

Next time: more interactive React!

info340wi24-interactive-react

By Tim Carlson

info340wi24-interactive-react

  • 69