<info 340/>

Interactive React
(continued)

Tim Carlson
Spring 2024

View of the Day

  • Q&A (pollev)

  • Review: Chat Interactivity so far

  • Adding more interactivity (switching users!)

  • React Libraries

Questions?

Conditional Rendering Example

function MessageItem(props) {
  const { userName, userImg, text } = props.messageData;
  const [isLiked, setIsLiked] = useState(false);

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

  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>
        { isLiked &&
          <div>
          	<p> Here's some more text if the 'liked' button is selected!</p>
          	<img src={userImg} alt={userName + "'s avatar"} />
          </div>
        }
      </div>
    </div>
  )
}

Example 2: If isLiked, then show that extra div section

Example 1: If isLiked, change the color on the isLiked button

How do we make a filter option that uses checkboxes?

export function ChatPane(props) {
  const [parrotFilter, setParrotFilter] = useState(false);
  const currentChannel = props.currentChannel;
  const chatMessages = props.chatMessages;
...
  const parrotFilteredMessages = channelMessages.filter((msgObj) => {
    if (!parrotFilter)
      return (true);
    else
      return (parrotFilter && msgObj.userName === "Parrot");
  })

  const handleChange = (event) => {
    setParrotFilter(!parrotFilter);
  }

  const messageItemArray = parrotFilteredMessages.map((messageObj) => {
    const element = (
      <MessageItem
        messageData={messageObj}
        key={messageObj.timestamp}
      />
    )
    return element;
  })
...
  return (
    <div className="scrollable-pane">
      <label>
        <input type="checkbox" id="parrotFilter" name="parrotFilter" onChange={handleChange} />
        {" Parrot Filter"}
      </label>

      <div className="pt-2 my-2">
        {messageItemArray}
      </div>
    </div>
  )
}

state var

if filter is checked

then filter for "Parrot"

filter input element

Our chat app so far...

Let's make a chat app!

isLiked Example of State

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

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

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

  //RENDERING
  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 setter function

setState when clicked (toggles)

Allows us to change the look when rendered

style attribute is applied

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. 

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

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

initial value for variable

React Controlled Form Inputs

import React, { useState } from 'react';

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

  const handleChange = (event) => {
    const inputtedValue = event.target.value;
    setTypedValue(inputtedValue); //update state and re-render!
  }

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log("submitting", typedValue);
    const userObj = { userId: "parrot", userName: "Parrot", userImg: "/img/Penguin.png" }
    props.addMessageCallback(userObj, typedValue, "general")
    setTypedValue(""); //empty the input!

  }

  return (
    <form className="my-2" onSubmit={handleSubmit}>
      <div className="input-group">
        <textarea className="form-control" rows="2" 
          placeholder="Type a new message" 
          onChange={handleChange} value={typedValue}></textarea>
        <button className="btn btn-secondary" type="submit">
          <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 state var with  value we get back in the event prop

prevent default

React Form Submit

import React, { useState } from 'react';

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

  const handleChange = (event) => {
    const inputtedValue = event.target.value;
    setTypedValue(inputtedValue); //update state and re-render!
  }

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log("submitting", typedValue);

    const userObj = { userId: "parrot", userName: "Parrot", userImg: "/img/Penguin.png" }
    props.addMessageCallback(userObj, typedValue, "general")
    setTypedValue(""); //empty the input!
  }

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

event handler when 'submit' is clicked

use the typedValue to create object that you can add to the message array stored in App

empty the state var

Props vs State

  1. Is the value passed in from a parent via props? If so, it probably isn’t state.
     
  2. Does the value remain unchanged within a single page viewing? 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).

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)

<ChildB data={data} />

Has Data (state)

Lift 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(), etc }
    const updateChatMessages = [...chatMessages, newMessage];
    setChatMessages(updateChatMessages);
  }
  
  ...

chat state elevated to the App

pass down addMessage prop

pass down chatMessage  prop

addMessage implemented in the App

Passing Callbacks

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.

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>
  )
}

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!
  }
  
  //...
}

Create a new temp array or object to pass to setState

export default function App(props) {
  const [chatMessages, setChatMessages] = useState(CHAT_HISTORY);
  const channelList = [
    'general', 'random', 'dank-memes', 'channel-4', 'pet-pictures']
  const currentChannel = "general";

  const addMessage = (userObj, messageText, channel) => {
    const newMessage = {
      "userId": userObj.userId,
      "userName": userObj.userName,
      "userImg": userObj.userImg,
      "text": messageText,
      "timestamp": Date.now(),
      "channel": channel
    } 

////this below won't work because even though the new message to the
    //end of the array and call setState, react doesn't know that
    //the array has changed and doesn't rerender

    // chatMessages.push(newMessage);
    // setChatMessages(chatMessages); //update state and re-render
    // console.log(chatMessages)
  
    const updateChatMessages = [...chatMessages, newMessage];
    setChatMessages(updateChatMessages); //update state and re-render
  }

Note I create a new temporary array to pass to the setState variable

const DEFAULT_USERS = [
  {userId: null, userName: null, userImg: '/img/null.png'}, //null user
  {userId: "penguin", userName: "Penguin", userImg: '/img/Penguin.png'},
  {userId: "parrot", userName: "Parrot", userImg: '/img/Parrot.png'},
  {userId: "zebra", userName: "Zebra", userImg: '/img/Zebra.png'},  
]

export function HeaderBar(props) {
  const handleClick = (event) => {
    const whichUser = event.currentTarget.name //access button, not image
    console.log(whichUser);
    const selectedUserObj = DEFAULT_USERS.filter((userObj) => userObj.userId === whichUser)[0] 
    	|| DEFAULT_USERS[0] //null user if not found
   }

  const userButtons = DEFAULT_USERS.map((userObj) => {
    return (
      <button className="btn user-icon" key={userObj.userName} 
        name={userObj.userId} onClick={handleClick}
      >
        <img src={userObj.userImg} alt={userObj.userName + " avatar"} />
      </button>
    )
  })

  return (
    <header className="text-light bg-primary px-1 d-flex justify-content-between">
      <h1>React Messenger</h1>
      <div>
        {userButtons}
      </div>
    </header>
  )
}

Header - with users

Array of users

transform user obj array to array of buttons

click event if user button clicked

skeleton callback when clicked

export default function App(props) {
  //state
  const [chatMessages, setChatMessages] = useState(CHAT_HISTORY);
  const [currentUser, setCurrentUser] = 
      useState({userId: null, userName: null, userImg: '/img/null.png'});
  //initialize as null user

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

 ...

  //what content should my App look like?
  return (
    <div className="container-fluid d-flex flex-column">
      <HeaderBar currentUser={currentUser}/>
      <div className="row flex-grow-1">
        <div className="col-3">
          <ChannelList channels={channelList} currentChannel={currentChannel} />
        </div>
        <div className="col d-flex flex-column">
          <ChatPane chatMessages={chatMessages} currentChannel={currentChannel} />
          <ComposeForm currentUser={currentUser} addMessageCallback={addMessage} />
        </div>
      </div>
    </div>
  );
}

State - (user state to the app) 1

State var for current user initialized to null user

pass down current user

export function HeaderBar(props) {

  const currentUser = props.currentUser;

  const handleClick = (event) => {
    const whichUser = event.currentTarget.name //access button, not image
    console.log(whichUser);
    const selectedUserObj = DEFAULT_USERS.filter((userObj) => userObj.userId === whichUser)[0] || DEFAULT_USERS[0] //null user if not found
  }

  const userButtons = DEFAULT_USERS.map((userObj) => {
    //if currently logged in
    let classList = "btn user-icon";
    if (userObj.userId === currentUser.userId) {
      classList = "btn user-icon highlighted"
    }

    return (
      <button className={classList} key={userObj.userName}
        name={userObj.userId} onClick={handleClick}
      >
        <img src={userObj.userImg} alt={userObj.userName + " avatar"} />
      </button>
    )
  })

  return (
    <header className="text-light bg-primary px-1 d-flex justify-content-between">
      <h1>React Messenger</h1>
      <div>
        {userButtons}
      </div>
    </header>
  )
}

State - User State to App 2

Get current user from prop

style current user button differently

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

  const currentUser = props.currentUser;

  const handleChange = (event) => {
    const inputtedValue = event.target.value;
    setTypedValue(inputtedValue); //update state and re-render!
  }

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log("submitting", typedValue);

    const userObj = { userId: "parrot", userName: "Parrot", userImg: "/img/Penguin.png" }
    props.addMessageCallback(userObj, typedValue, "general")
    setTypedValue(""); //empty the input!
  }

  return (
    <form className="my-2" onSubmit={handleSubmit}>
      <div className="input-group">
        {<img src={currentUser.userImg} alt={currentUser.userName + " avatar"} />}
        <textarea className="form-control" rows="2" 
          placeholder="Type a new message" 
          onChange={handleChange} value={typedValue}></textarea>
        <button className="btn btn-secondary" type="submit">
          <span className="material-icons">send</span>
        </button>
      </div>
    </form>
  );
}

State - User State to App 3

Get current user from prop

use current user pic in control

export default function App(props) {
  ...
  const [currentUser, setCurrentUser] = 
      useState({userId: null, userName: null, userImg: '/img/null.png'});
  
...

  const loginUser = (userObj) => {
    console.log("logging in as: ", userObj.userName);
    setCurrentUser(userObj);
  }


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

State - User State to App 4

function to update user in App

Pass the callback to Header as a callback

export function HeaderBar(props) {

  const currentUser = props.currentUser;
  const loginFunction = props.loginUserCallback;

  const handleClick = (event) => {
    const whichUser = event.currentTarget.name //access button, not image
    console.log(whichUser);
    const selectedUserObj = DEFAULT_USERS.filter((userObj) => userObj.userId === whichUser)[0] || DEFAULT_USERS[0] //null user if not found
    console.log(selectedUserObj);
    loginFunction(selectedUserObj);
    
  }

...

  return (
    <header className="text-light bg-primary px-1 d-flex justify-content-between">
      <h1>React Messenger</h1>
      <div>
        {userButtons}
      </div>
    </header>
  )
}

State - User State to App 5

get update function from prop

Update when the user clicks on button

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

  const currentUser = props.currentUser;

  const handleChange = (event) => {
    const inputtedValue = event.target.value;
    setTypedValue(inputtedValue); //update state and re-render!
  }
  const handleSubmit = (event) => {
    event.preventDefault();
    console.log("submitting", typedValue);

    // const userObj = { userId: "parrot", userName: "Parrot", userImg: "/img/Penguin.png" }
    props.addMessageCallback(currentUser, typedValue, "general")
    setTypedValue(""); //empty the input!
  }

  return (
    <form className="my-2" onSubmit={handleSubmit}>
      <div className="input-group">
        {<img src={currentUser.userImg} alt={currentUser.userName + " avatar"} />}
        <textarea className="form-control" rows="2" 
          placeholder="Type a new message" 
          onChange={handleChange} value={typedValue}></textarea>
        <button className="btn btn-secondary" type="submit">
          <span className="material-icons">send</span>
        </button>
      </div>
    </form>
  );
}

State - User State to App 6

Use currentUser rather than hack temporary object

export default function App(props) {

  const [chatMessages, setChatMessages] = useState(CHAT_HISTORY);
  const [currentUser, setCurrentUser] = 
      useState({userId: null, userName: null, userImg: '/img/null.png'});

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

  const addMessage = ( messageText) => {
    const userObj = currentUser;
    const newMessage = {
      "userId": userObj.userId,
      "userName": userObj.userName,
      "userImg": userObj.userImg,
      "text": messageText,
      "timestamp": Date.now(),
      "channel": currentChannel
    } 
...
}

State - (moving to the app) 7

Use state vars. Don't need the caller to provide currentUser or current Channel

export function ComposeForm(props) {
...
  const handleSubmit = (event) => {
    event.preventDefault();
    // const userObj = { userId: "parrot", userName: "Parrot", userImg: "/img/Penguin.png" }
    props.addMessageCallback(typedValue)
    setTypedValue(""); //empty the input!
  }

Only need to provide text arg

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>);
  )
}

React Libraries

React components are structured to be self-contained, re-usable elements... so there are lots of pre-defined components online you can use!

In order to use a component in your app:

  1. Find it! (via npm, google, etc). Read the documentation
  2. Install it! npm install lib-name
  3. Import it! import { ComponentName } from 'lib-name'
    • (import structure may vary per library)
  4. Render it! <ComponentName />
    • Remember to pass any expected props!

External Libraries

Much of the web (and software in general) is built on shared, reusable libraries.

Bootstrap style Dropdowns 

//Slide 29, User the callback to swap users
import React from 'react';
import Dropdown from 'react-bootstrap/Dropdown';
...

  const userButtons = DEFAULT_USERS.map((userObj) => {
    let classList = "btn user-icon";
    return (
      <Dropdown.Item className={classList} key={userObj.userName}
        name={userObj.userId} onClick={handleClick}
      >
        <img src={userObj.userImg} alt={userObj.userName + " avatar"} />
        {userObj.userName}
      </Dropdown.Item>
    )
  })

  return (
    <header className="text-light bg-primary px-1 d-flex justify-content-between">
      <h1>React Messenger</h1>

      <Dropdown>
      <Dropdown.Toggle variant="primary">
      <img src={currentUser.userImg} alt={currentUser.userName + " avatar"} />
      </Dropdown.Toggle>

      <Dropdown.Menu>
        {/* <Dropdown.Item href="#/action-1">Action</Dropdown.Item>
        <Dropdown.Item href="#/action-2">Another action</Dropdown.Item>
        <Dropdown.Item href="#/action-3">Something else</Dropdown.Item> */}
        {userButtons}
      </Dropdown.Menu>
    </Dropdown>
    </header>
  )
}

import react-bootstrap

transform json user objects  into Dropdown.Items

add current user to DropDown.Toggle

Make Channel Changing interactive (app changes)

//Slide 30, User the callback to swap users
import React, { useState } from 'react';
...
export default function App(props) {
  
  const [chatMessages, setChatMessages] = useState(CHAT_HISTORY);
  const [currentUser, setCurrentUser] = 
      useState({userId: null, userName: null, userImg: '/img/null.png'});
  
  const channelList = [
    'general', 'random', 'dank-memes', 'channel-4', 'pet-pictures'
  ]
  // const currentChannel = "general";
  const [currentChannel, setCurrentChannel] = useState("general")

  const updateCurrentChannel = (channelName) =>  {
    setCurrentChannel(channelName);
  }

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

currentChannel as state var

function to update current channel

pass updater function as a prop to ChannelList

Make Channel Changing interactive (ChannelList changes)

/ Slide 31 Make channelist changing interactive
import React from 'react';

export function ChannelList(props) {

  const channels = props.channels;
  const currentChannel = props.currentChannel;

  const handleClick = (event) => {
    // console.log("event: ", event)
    console.log("event.textContent ", event.target.value)
    props.updateCurrentChannel(event.target.value);
  }

  const linkElemArray = channels.map((channelNameString) => {

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

    const element = (
      <div key={channelNameString} >
        <button className={classList} value={channelNameString} onClick={handleClick}>
    			{channelNameString}</button>
      </div>
    )
    return element;
  })

click handler function

call updater which was passed down as prop

register onClick

Find the count of "Liked Messages"

//Slide 32 - Find the count of liked messages from the data of objects
...

export function ChatPane(props) {
  ...
  const channelMessages = chatMessages
    .filter((msgObj) => {
      return msgObj.channel === currentChannel;
    })
    .sort((a,b) => b.timestamp - a.timestamp);

    const countOfLikedMessages = channelMessages.filter((msgObj) => {
      return msgObj.liked === true;
    }).length

...

  return (
    <div className="scrollable-pane">
      <p>Number of liked messages: {countOfLikedMessages}</p>
      <div className="pt-2 my-2">
        {messageItemArray}
      </div>
    </div>
  )
}

find # of liked from channelMessages

display the count

But MessageItem has local state variable for isLiked

//Slide 33 - But the 'liked' state here is local to the MessageItem Component
...

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

  //local state
  const [isLiked, setIsLiked] = useState(liked);

  ...
  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>
  )
}

But this liked state variable is local to the MessageItem component

Make the data in App (parent) be the owner

//Slide 34 - Add a setter to toggle isLiked in the data of the Parent. Then pass 
// the setter down to as a prop to the eventHandler rather than setting local state variable

export default function App(props) {
  const [chatMessages, setChatMessages] = useState(CHAT_HISTORY);
    ...
    const toggleIsLiked = (timestamp) => {
    const copyOfChatMessages = chatMessages.map((chatObj) => {
      if (chatObj.timestamp === timestamp)
        return {...chatObj, "liked": !chatObj.liked}
      else 
      return {...chatObj}
    } ) 
 setChatMessages(copyOfChatMessages);
}
   
return (
 <div className="container-fluid d-flex flex-column">
   <HeaderBar currentUser={currentUser} loginUserCallback={loginUser}/>
   <div className="row flex-grow-1">
    <div className="col-3">
     <ChannelList channels={channelList} updateCurrentChannel={updateCurrentChannel} currentChannel={currentChannel} />
    </div>
   <div className="col d-flex flex-column">
    <ChatPane chatMessages={chatMessages} currentChannel={currentChannel} toggleIsLikedCallback={toggleIsLiked} />
    <ComposeForm currentUser={currentUser} addMessageCallback={addMessage} />
   </div>
   </div>
 </div>
  );
}

create toggler/setter in App

pass the toggler/setter as prop

Pass the toggler/setter prop down 

//Slide 35/36 - Pass the setter/toggler down to the component that has the event handler

import React, { useState } from 'react';

export function ChatPane(props) {
  const {currentChannel, chatMessages, toggleIsLikedCallback} = props;
  
    const channelMessages = chatMessages
    .filter((msgObj) => {
      return msgObj.channel === currentChannel;
    })
    .sort((a,b) => b.timestamp - a.timestamp);

    const countOfLikedMessages = channelMessages.filter((msgObj) => {
      return msgObj.liked === true;
    }).length

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

get the prop

pass it down again to prop

Replace the logic for local state variable

//Slide 35/36 - Replace the local state variable with the setter to just update
// the status of the data in the data array held in the App ... which is the parent

unction MessageItem(props) {
  const {userName, userImg, text, liked, timestamp} = props.messageData;
  const toggleIsLikedCallback = props.toggleIsLikedCallback;

  // const [isLiked, setIsLiked] = useState(liked);

  const handleClick = (event) => {
        toggleIsLikedCallback(timestamp)
    // setIsLiked(!isLiked); //toggle
  }

  ...
  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>
  )
}

instead update the data in the parent (app) which will cause rerender

get the prop

remove the local state var

"Protestware"

Action Items!

Action Items!

  • Review Ch 15-16: React & Interactive React

  • Read Ch 17: Client-Side Routing

  • Problem Set 07 due Thursday (2 days)

    • Treat this as a hard deadline!

  • Project Draft 2 due Friday (3 days)

    • This is a hard deadline.

 

Next time: Router (multiple pages & navigation!)

info340Sp24-interactive-react-2

By Tim Carlson

info340Sp24-interactive-react-2

  • 43