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.
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
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';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
import React from 'react';
export default function App(props) {
//what should my app look like
return (
<div>Hello App</div>
);
}
App.js
index.js
export default function App(props) {
return (
<div>
<HeaderBar />
<ChannelList />
<ChatPane>
<MessageList />
<Message />
<Message />
</MessageList >
<ComposeForm />
</ChatPane>
</div>
)
};
export function HeaderBar(props){
return (
<header className="text-light bg-primary py1">
<h1> React Messenger</h1>
</header>
)
}
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 12
example 12a
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
export default function App(props) {
return (
<div>
<HeaderBar />
<ChannelList />
</div>
)
};
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
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)
[
{
"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>
)
}
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>
)
}
map the array of chat objects to array of <MessageItems>
use inline expression to enumerate the array of MessageItems
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
// 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>
)
};
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
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)
})
...
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>
)
};
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>;
}
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.
State is reserved only for interactivity, that is, data that changes over time
Some examples of state data:
//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.
//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
In order to write correct React (that can be understood
and debugged by you and others), you should follow the naming conventions:
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>
);
}
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>
);
}
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 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 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).
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).
Read/Review Ch 16-17: React & Interactive React
Problem Set 07 due next Thursday (5/16)
Don't put it off!
Project Draft 2 due 5/17
Convert Draft 1 into React Components!
Add one interactive feature
Lab: project management for draft 2
Next time: (more) Interactive React!