Let's do a PolChat!
Demo app: https://dariaoldenburg.github.io/au-react-workshop/
yarn
npm start
# Open http://localhost:3000# Launches the test runner in the interactive watch mode.
# See https://facebook.github.io/create-react-app/docs/running-tests for more information.
npm test// JS
const app = React.createElement(App);
ReactDOM.render(App, document.getElementById('root'));
// JSX
const app = <App />;
ReactDOM.render(app, document.getElementById('root'));
// JSX (shorter)
ReactDOM.render(<App />, document.getElementById('root'));// JS
React.createElement('button', {
className: 'special-button',
type: 'submit',
disabled: true,
children: 'Click me',
onClick: () => alert('clicked'!)
})
// JSX
<button
className="special-button"
type="submit"
disabled
onClick={() => alert('clicked'!)}>Click me</button>
// JS
React.createElement(App, { env: 'development' })
// JSX
<App env="development" />// bad
<button class="special-button" style={{ 'margin-left': '20px' }} tab-index="0">click</button>
// good
<button className="special-button" style={{ marginLeft: '20px' }} tabIndex={0}>click</button>
// P.S. In `style`, you can pass number instead of `...px`:
<button style={{ marginLeft: 20 }}>click</button>function Button(props: { label: string; onClick?(): void }) {
return <button onClick={props.onClick}>{props.label}</button>;
}
// Usage
<Button label="Click me!" onClick={() => alert('clicked!')} />;interface ButtonProps {
label: string;
onClick?(): void;
}
function Button(props: ButtonProps) {
return <button onClick={props.onClick}>{props.label}</button>;
}interface ButtonProps {
label: string;
disabled: boolean;
onClick?(): void;
}
function Button(props: ButtonProps) {
return (
<button disabled={props.disabled} onClick={props.onClick}>
{props.label}
</button>
);
}
Button.defaultProps = {
disabled: false
};-
Create
Chatcomponent that renders "I am a chat!" -
Render
<Chat />inApp -
Create
RowandCellcomponents with following props:interface RowProps { children?: React.ReactNode; } interface CellProps { header: boolean; padded: boolean; scrollable: boolean; textAlign: 'left' | 'center' | 'right'; widthPercentage: number; height?: number; children?: React.ReactNode; }
-
Make
Rowrender just adiv.Rowthat renders the children in it -
Make
Cellrender children in a div, that has following CSS class:Cell- alwaysCell--header- whenprops.headeris trueCell--padded- whenprops.paddedis trueCell--scrollable- whenprops.scrollableis trueCell--textAlign-left- whenprops.textAlign === 'left'(and the same for other textAlign values)
-
In
Cell, set the div's height toprops.heightpx, andprops.widthPercentage% -
In
Cell, provide default prop values for all the props besidesheightandchildren
-
-
In
Chat, use theRowandCellcomponents to create chat layout, for example:-
first row composed of two cells:
- chat title
- online users count
-
second row composed of two cells:
- chat messages
- online users list
-
third row composed of two cells:
- textarea
- login button
Note: You don't have to create any components for the inside stuff of the chat! Instead, you can just print in cells something like
"Here be chat title";)
-
-
(optionally) Create your own
Buttoncomponent
Docs: Converting a Function to a Class
// Simple component with no props
class Button extends React.Component {
render() {
return 'I am a button';
}
}
// With props
interface ButtonProps {
label: string;
onClick?(): void;
}
class Button extends React.Component<ButtonProps> {
render() {
return <button onClick={props.onClick}>{props.label}</button>;
}
}Docs: Adding Local State to a Class
Implementing Accordion:
interface AccordionProps {
title: string;
description: string;
}
interface AccordionState {
extended: boolean;
}
class Accordion extends React.Component<AccordionProps, AccordionState> {
state: AccordionState = {
extended: false
};
render() {
const { title, description } = this.props;
const { extended } = this.state;
return (
<div className="Accordion">
<div
className="Accordion__title"
onClick={() => this.setState({ extended: !extended })}
>
{title}
</div>
{extended && (
<div className="Accordion__description">{description}</div>
)}
</div>
);
}
}// Better
class Accordion extends React.Component<AccordionProps, AccordionState> {
state: AccordionState = {
extended: false
};
// Note: binding `this` is important here!
handleTitleClick = () => {
this.setState(state => {
extended: !state.extended;
});
};
render() {
const { title, description } = this.props;
const { extended } = this.state;
return (
<div className="Accordion">
<div className="Accordion__title" onClick={this.handleTitleClick}>
{title}
</div>
{extended && (
<div className="Accordion__description">{description}</div>
)}
</div>
);
}
}Docs: Adding Lifecycle Methods to a Class
interface ClockState {
date: Date;
}
class Clock extends React.Component<{}, ClockState> {
state: ClockState = {
date: new Date()
};
private timerID?: NodeJS.Timeout;
componentDidMount() {
this.timerID = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.timerID!);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return <div>Current time: {this.state.date.toLocaleTimeString()}</div>;
}
}When rendering a list of elements, remember to add a unique key prop to all of them.
// Bad: Invalid JSX
return (
<Component data={userA} />
<Component data={userB} />
<Component data={userC} />
);
// Bad: `key` is missing
return [
<Component data={userA} />,
<Component data={userB} />,
<Component data={userC} />,
];
// Good
return [
<Component key={userA.id} data={userA} />,
<Component key={userB.id} data={userB} />,
<Component key={userC.id} data={userC} />,
];
// Better
return [userA, userB, userC].map(user => (
<Component key={user.id} data={user} />
));P.S. When rendering a non-dynamic list, you can just use <React.Fragment> or <> instead (then key is not needed):
return (
<>
<Component data={userA} />
<Component data={userB} />
<Component data={userC} />
</>
);Now we can add messages list.
- Create
MessagesListcomponent with following props:
interface MessagesListProps {
messages: Message[];
}-
Use map function for mapping all messages. Try to make the html structure for the message similar to the one in the demo app. You can use div, strong and span tags.
-
To render the date you can use:
const date = new Date(message.createdAt);
date.toLocaleTimeString();-
For this component you don't need any css classes, you can just add inline styles (color and font-weight) for username.
-
In
ChatrenderMessagesList
It's time to add login and logout functionalities.
-
In
Chat:-
add
currentUser: User | nulltothis.state -
if currentUser exists then render a button that triggers
this.handleSignOutwhen clicked -
if currentUser doesn't exists then render a button that triggers
this.handleSignInwhen clicked -
create
this.handleSignInmethod. Useprompt()to display a dialog boxes. You should ask an user for name and color. If the user gives the name, uselogin()fromChatAPI, otherwise show alert. When Promise is resolved set current user. -
create
this.handleSignOutmethod. Uselogout()fromChatAPIand set current user to null.
-
- Render users list.
class NameForm extends React.Component<
{ initialValue: string },
{ value: string }
> {
constructor(props) {
super(props);
this.state = { value: '' };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event: React.ChangeEvent<HTMLInputElement>) {
this.setState({ value: event.target.value });
}
handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
alert('A name was submitted: ' + this.state.value);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input
type="text"
value={this.state.value}
onChange={this.handleChange}
/>
</label>
<input type="submit" value="Submit" />
</form>
);
}
}Use value instead of children.
<textarea value={this.state.value} onChange={this.handleChange} />// Bad
class Chat extends React.Component<
{},
{
newMessageContent: string;
}
> {
state = {
newMessageContent: ''
};
render() {
return (
<NewMessageForm
content={this.state.newMessageContent}
updateState={this.setState}
/>
);
}
}
// Good
class Chat extends React.Component<
{},
{
newMessageContent: string;
}
> {
state = {
newMessageContent: ''
};
handleContentChange = (nextContent: string) => {
this.setState({ newMessageContent: nextContent });
};
render() {
return (
<NewMessageForm
content={this.state.newMessageContent}
onContentChange={this.handleContentChange}
/>
);
}
}// Bad
interface Props {
message: Message;
onChange?(message: Message): void;
}
interface State {
inEdit: boolean;
}
class MessagePage extends React.Component<Props, State> {
state: State = {
inEdit: false
};
render() {
const { message, onChange } = this.props;
const { inEdit } = this.state;
return (
<>
<MessagePreview message={message} />
{inEdit && (
<textarea
value={message.content}
onChange={event => {
message.content = event.target.value;
if (onChange) {
onChange(message);
}
}}
/>
)}
</>
);
}
}
// Good
interface Props {
message: Message;
onChange?(message: Message): void;
}
interface State {
inEdit: boolean;
}
class MessagePage extends React.Component<Props, State> {
state: State = {
inEdit: false
};
render() {
const { message, onChange } = this.props;
const { inEdit } = this.state;
return (
<>
<MessagePreview message={message} />
{inEdit && (
<textarea
value={message.content}
onChange={event => {
if (onChange) {
onChange({ ...message, content: event.target.value });
}
}}
/>
)}
</>
);
}
}
// Good as well
interface Props {
message: Message;
onChange?(message: Message): void;
}
interface State {
inEdit: boolean;
editedMessage: Message;
}
class MessagePage extends React.Component<Props, State> {
state: State = {
inEdit: false,
editedMessage: this.props.message
};
componentDidUpdate(prevProps: Props) {
if (prevProps !== this.props.message) {
this.setState({
editedMessage: this.props.message
});
}
}
render() {
const { onChange } = this.props;
const { editedMessage, inEdit } = this.state;
return (
<form
onSubmit={event => {
event.preventDefault();
onChange(editedMessage);
}}
>
<MessagePreview message={editedMessage} />
{inEdit && (
<textarea
value={editedMessage.content}
onChange={event => {
this.setState({
editedMessage: { ...editedMessage, content: event.target.value }
});
}}
/>
)}
</form>
);
}
}Add a possibility to write and submit new messages:
-
Create
MessageBoxcomponent, that:-
has following props:
interface MessageBoxProps { messageContent?: string; disabled?: boolean; onChange?: (value: string) => void; onSubmit?: () => void; }
-
renders
<textarea>, that:- has value equal to
props.messageContent - triggers
props.onChangewhenever its' value is changed
- has value equal to
-
wraps
<textarea>with<form>, that:- triggers
props.onSubmitwhen form is submitted
- triggers
-
renders a button, that triggers
props.onSubmitwhen clicked -
when
<textarea>receivesENTER(but notSHIFT+ENTER), it triggersprops.onSubmit
-
-
In
Chat:-
add
newMessageContent: stringtothis.state -
render
MessageBoxand controlthis.state.newMessageContentwith it -
once
MessageBox#props.onSubmitis called, callChatApi.createMessage()
-
-
Render nested thread messages (
Message.submessages) -
Add possibility to add a message to an existing thread (
parentMessageIdinChatApi.createMessage(...))
import React from 'react';
import ReactDOM from 'react-dom';
import { Button } from './Button';
import { Simulate } from 'react-dom/test-utils';
it('renders the button element', () => {
const div = document.createElement('div');
ReactDOM.render(<Button>text</Button>, div);
const button = div.querySelector('button')!;
expect(button).toBeTruthy();
expect(button.textContent).toEqual('text');
ReactDOM.unmountComponentAtNode(div);
});
it('calls onClick on click', () => {
const onClick = jest.fn();
const div = document.createElement('div');
ReactDOM.render(<Button onClick={onClick} />, div);
const button = div.querySelector('button')!;
Simulate.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
ReactDOM.unmountComponentAtNode(div);
});//better
import React from 'react';
import ReactDOM from 'react-dom';
import { Button } from './Button';
import { Simulate } from 'react-dom/test-utils';
let div: HTMLDivElement;
beforeEach(() => {
div = document.createElement('div');
});
afterEach(() => {
ReactDOM.unmountComponentAtNode(div);
});
it('renders the button element', () => {
ReactDOM.render(<Button>text</Button>, div);
const button = div.querySelector('button')!;
expect(button).toBeTruthy();
expect(button.textContent).toEqual('text');
});
it('calls onClick on click', () => {
const onClick = jest.fn();
ReactDOM.render(<Button onClick={onClick} />, div);
const button = div.querySelector('button')!;
Simulate.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
});- Create test for
Cell. You only need to check if it renders correctly. - Create similar test for
Row.
- Create test for another component from outside ui directory.
This project was bootstrapped with Create React App.
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
git checkout lesson1 # Lesson 1
git checkout lesson2 # Lesson 2
git checkout lesson3 # Lesson 3
git checkout lesson4 # Lesson 4
git checkout master # After all lessons
