<info 340/>

React Apps

Tim Carlson

Autumn 2024

View of the Day

  • Building an App from scratch (code demo)

    • Defining components
    • Passing data as props
    • Converting data into views!
  • Intro to Interactivity (if time)

Questions?

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 next week)

Vite Dev Server

Vite provides a development server which will:

  • Automatically transpile React code into pure JavaScript
  • Manage module dependencies, including external libraries
  • Show build and syntax errors in the console
  • Automatically reload the page (replaces live-server)!
# Make sure you are in the project directory
cd path/to/project

# Install dependencies for existing project (installs vite)
npm install

# Run the development server script
npm run dev

React Components

We define components as functions that return the DOM elements to be rendered

//declare a function to define a component -- this is like a class
function HelloMessage(props) {

  
  //this function returns the elements (JSX)
  //that make up the component
  return (
     <h1>Hello World!</h1>
  );
}

//"call" function to create a new element value!
const msgElem = <HelloMessage />;

//show the content in the web page (inside #root)
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(msgElem)

what is rendered when
component is shown

our own HTML tags!

Capitalize!

Properties (props)

function MessageItem(props) {
   const message = props.message; //access the prop



    //can use prop for logic or processing
   const messageUpper = message.toUpperCase(); 

   return <li>{messageUpper}</li>; //render based on prop
}

ReactDOM.createRoot(document.getElementById('root'))
   .render(<MessageItem message="Be quiet" />)

Inside the Component function definition, all the passed in props are passed in as a single argument object (conventionally called props)

ALL props stored in this object

Props and Composition

function MessageList(props) {
  //msgComponents will be an array of components!
  const msgComponents = props.messages.map((msgStr) => {
    const elem = <MessageItem message={msgStr} key={msgStr} />; //pass prop down!
    return elem
  }

  return (
    <ul>
      {/* An array of components renders as siblings */}
      {msgComponents} 
    </ul>
  );
}

const messagesArray = ["Hello world", "No borders", "Go huskies!"];

ReactDOM.createRoot(document.getElementById('root'))
   .render(<MessageList messages={messagesArray} />)

Props will often need to be "passed down" to child components. A common pattern is to map an array of prop values to an array of children components to render!

unique "id" for the element

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

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

import App from './components/App.jsx';

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

export default function App(props) {
  
  //what should my app look like
  return (
    <div>Hello App</div>
  );
}

App.js

index.js

Bootstrap in React

To use Bootstrap (styling) in React, install the library as a dependency and then import the css file.

Install the dependency

# you must be in the React project folder
cd path/to/my-react-app

# install dependency
npm install bootstrap
/* in index.js */
import 'bootstrap/dist/css/bootstrap.css';
import 'my-style.css' // import your stylesheet afterwards

Import the stylesheet

Let's make a chat app!

We can break into Components

psuedo code components

export default function App(props) {

  return (
    <div>
      <HeaderBar />
      <ChannelList />
      <ChatPane>
    	<MessageList />
        	<Message />
        	<Message />
    	</MessageList >
        <ComposeForm />
      </ChatPane>

    </div>
  )
};

HeaderBar with styles

export function HeaderBar(props){
    return (
      
      
        <header className="text-light bg-primary py1">
            <h1> React Messenger</h1>
        </header>
    )
}

ChannelList with styles

import React from "react";

export function ChannelList(props) {
    return (
        <nav className="bg-secondary text-light py3">
            <ul>
                <li>General</li>
                <li>Social</li>
                <li>Dank Memes</li>
                <li>Channel-4</li>
            </ul>
        </nav>

    )
}

example 15

example 15a

export function ChannelList(props) {
  const CHANNEL_LIST = ['general', 'social', 'dank-memes', 'channel-4'];

  const liArray = CHANNEL_LIST.map((channelNameString) => {
    const element = <li key={channelNameString}><a className='text-light' href="">{channelNameString}</a></li>
    return element;
  })
    
  return (
      <nav className="bg-secondary text-light py3">
          <ul>
              {liArray}
          </ul>
      </nav>
    )
}

map the array of strings to array of styled <li> elements

use inline expression to enumerate the array of styled <li>'s

App at this point

export default function App(props) {

  return (
    <div>

      <HeaderBar />
      <ChannelList />
      
    </div>
  )

};

ChatPane

import React from 'react';

export  function ChatPane(props) {
    return  (
         <div>
            <MessageItem />
            <MessageItem />
            <MessageItem />
            <MessageItem />

         </div>
    )
}
function MessageItem(props) {
    return (
        <div className='message'>
            <img src="/img/parrot.png" alt="Parrot's profile"/>
            <p>Parrot</p>
            <p>Squawk! Squawk! lorem ipsum</p>
        </div>
    )
}
.message img {
    border-radius: 50%;
}

index.css

make the pictures

round

ChatPane

import React from 'react';

export  function ChatPane(props) {
    return  (
         <div>
            <MessageItem />
            <MessageItem />
            <MessageItem />
            <MessageItem />

         </div>
    )
}
function MessageItem(props) {
    return (
        <div className='message d-flex'>
            <div className='me-2'>
                <img src="/img/parrot.png" alt="Parrot's profile" />
            </div>
            <div className='flex-grow-1'>
                <p className='user-name'>Parrot</p>
                <p>Squawk! Squawk! lorem ipsum</p>
            </div>
        </div>
    )
}
.message img {
    width:50px;
    height: 50px;
    border-radius: 50%;
}

.message .user-name {
    font-weight: bold;
    margin: 0;
}

index.css

more styling

use <div>'s to organize the <messageItem>

make d-flex (flexbox)

Data: chat_log.json

[
  {
      "userId": "penguin",
      "userName": "Penguin",
      "userImg": "/img/Penguin.png",
      "text": "It's so cold out today!",
      "timestamp": 1320161040000,
      "channel": "general"
  },
  {
      "uid": "parrot",
      "userName": "Parrot",
      "userImg": "/img/Parrot.png",
      "text": "It's warm over here",
      "timestamp": 1320161760000,
      "channel": "general"
  },
  
  ...
  
   {
      "userId": "owl",
      "userName": "Owl",
      "userImg": "/img/Owl.png",
      "text": "Who?",
      "timestamp": 1320224640000,
      "channel": "random"
  }
]
  
import CHAT_HISTORY from '../data/chat_log.json';
export function ChatPane(props) {
  
  return (
        <div className='pt-2'>
            <MessageItem messageData={CHAT_HISTORY[0]}/>
            <MessageItem messageData={CHAT_HISTORY[1]}/>
            <MessageItem messageData={CHAT_HISTORY[2]}/>
            <MessageItem messageData={CHAT_HISTORY[3]}/>
        </div>
    )
}

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

    const { userName, userImg, text} = props.messageData

    return (
        <div className='message d-flex mb-2'>
            <div className='me-2'>
                <img src={userImg} alt="userName+ " avatar" />
            </div>
            <div className='flex-grow-1'>
                <p className='user-name'>{userName}</p>
                <p>{text}</p>
            </div>
        </div>
    )
}

Chat - grabbing using external data

external chat data

can assign each independently

or decompose in one step

then use these variables with inline expression

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

export function ChatPane(props) {

    const messageItemArray = CHAT_HISTORY.map((messageObj) => {
        const element = <MessageItem messageData={messageObj} key={messageObj.timestamp}/>
        return element;
})
    return (
        <div className='pt-2'>
        {messageItemArray}  
        </div>     
    )
}

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

    return (
        <div className='message d-flex mb-2'>
            <div className='me-2'>
                <img src={userImg} alt={userName+ "avatar"} />
            </div>
            <div className='flex-grow-1'>
                <p className='user-name'>{userName}</p>
                <p>{text}</p>
            </div>
        </div>
    )
}

Chat - map the data instead

map the array of chat objects to array of <MessageItems>

use inline expression to enumerate the array of MessageItems

React Style Guidelines

App after some styling

export default function App(props) {

  return (
    <div>

      <HeaderBar />
      <div className='row'>
        <div className='col-4'>
          <ChannelList />
        </div>
        <div className='col-8'>
          <ChatPane />
        </div>
      </div>
    </div>
  )
};

put the ChannelList and the ChatPane in a flexbox so they are side by side

Style the App for layout

// Laying out the app
function App(props) {
  return (
    <div className="container-fluid">
      <HeaderBar />
      <div className='row'>
        <div className='col-2'>
          <ChannelList />
        </div>
        <main className='col'>
          <MessagePane />
        </main>
      </div>

      {/* <ComposeForm/> */}
    </div>
  )
};

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. Not recommended
  return (
    <div>
      {conditionOne == true && <OptionA />}
    </div>
  )
}

Channel

export function ChannelList() {
    const CHANNEL_LIST = ['general', 'social', 'dank-memes', 'channel-4'];

    const currentChannel = 'general';

    const liArray = CHANNEL_LIST.map((channelNameString) => {
        let classList = 'text-light';
        if (channelNameString === currentChannel){
            classList= 'text-dark bg-warning'
        }
        const element = <li key={channelNameString}><a className={classList} 
      		href="">{channelNameString}</a></li>
        return element;
    })
    
    return (
        <nav className="bg-secondary text-light py3 h-100">
            <ul>
                {liArray}
            </ul>
        </nav>
    )
}

conditional formatting to make the current channel have different formatting

Only show Channel msgs

const currentChannel = 'general';
const channelMessage = CHAT_HISTORY.filter((msgObj) => {
    //if current channel, then kep
    return (msgObj.channel === currentChannel)
})

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

    return (
        <div className='pt-2'>
        {messageItemArray}  
        </div>     
    )
}

function MessageItem(props) {
    const { userName, userImg, text} = props.messageData
    return (
        <div className='message d-flex mb-2'>
            <div className='me-2'>
                <img src={userImg} alt="Parrot's profile" />
            </div>
            <div className='flex-grow-1'>
                <p className='user-name'>{userName}</p>
                <p>{text}</p>
            </div>
        </div>
    )
}

filter to have only show the current channel in the ChatPane

export function ChannelList(props) {
    const CHANNEL_LIST = ['general', 'social', 'dank-memes', 'channel-4'];

    // const currentChannel = 'general';
    const currentChannel = props.currentChannel;
...
}
export function ChatPane(props) {

    const currentChannel = props.currentChannel;
    const channelMessage = CHAT_HISTORY.filter((msgObj) => {
        //if current channel, then keep
        return (msgObj.channel === currentChannel)
    })
    
    ...

State Example

Move the state variable up to the App

Pass it to the subcomponents as a prop

export default function App(props) {
  const currentChannel = 'general';

  return (
    <div className='d-flex flex-column'>

      <HeaderBar />
      <div className='row flex-grow-1'>
        <div className='col-3'>
          <ChannelList currentChannel={currentChannel} />
        </div>
        <div className='col-8'>
          <ChatPane currentChannel={currentChannel}/>
        </div>
      </div>
    </div>
  )
};
export default function App(props) {
  const currentChannel = 'general';

  return (
    <div className='d-flex flex-column'>

      <HeaderBar />
      <div className='row flex-grow-1'>
        <div className='col-3'>
          <ChannelList currentChannel={currentChannel} />
        </div>
        <div className='col-8'>
          <ChatPane currentChannel={currentChannel}/>
        </div>
      </div>
    </div>
  )
};

Interactivity

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

DOM Interactivity

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

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

//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

On button click, do 2 things:

Event Listeners in React

function MessageItem(props) {

    const { userName, userImg, text} = props.messageData

    const handleClick = (event) => {
        console.log("you clicked on", userName);
    }

    return (
        <div className='message d-flex mb-2'>
            <div className='me-2'>
                <img src={userImg} alt="Parrot's profile" onClick={handleClick} />
            </div>
            <div className='flex-grow-1'>
                <p className='user-name'>{userName}</p>
                <p>{text}</p>
            </div>
        </div>
    )
}

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

//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

Naming Conventions Matter!

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

  • The argument to a Component function is called props
     
  • 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 (for speed). Calling a "state-setter" function (when ready) and automatically re-render the Component (by calling the function again).

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

  const handleClick = (event) => {
    setCount(4); //change `count` to 4 AND re-render!
    console.log(count); //will output "3"; 
                        //state has not changed yet!
  }  
 
  console.log(count); //will have "current" value of state
                      //3 first render, 4 after clicking
  
  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); //incremenet 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) => {
      if(index == 0) { //transform objects if needed
        todoObject.text = "Fix bugs"
      }
      return todoObject; //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).

Action Items!

Action Items!

  • Complete task list for Week 7 (items 1-2 !!)

  • Read/review: Chapter 15-16

  • Problem Set 07 due next week

    • This is the biggest and hardest set!

    • You can do problem-a now!

    • Start now so you have time

 

Next: Adding interactivity!